Release/1.2.0#612
Merged
Merged
Conversation
…example seed (PR-6b) (#468) Runtime increment after PR-6a (#467). All opt-in via agent_type="skill"; the default "chat" path is unchanged, so zero default behavior change. 1. Fold gateway/external MCP tools behind the meta-tools. PR-6a folded only LOCAL callables; gateway/external tools materialize as *client objects* (one MCPClient exposing many tools), so they were available but not hidden. New mechanism: - mcp_tool_folding.py: a per-client fold set; both FilteredMCPClient and UICapableMCPClient drop folded names from list_tools_sync (the seam Strands uses to build the model tool list). Setting a fold also invalidates the client's _loaded_tools cache so Strands re-lists with the fold applied (external clients are pre-flighted before folds are known). - mcp_binding.py: resolve_mcp_bindings maps each non-local bound id to its concrete MCP tool(s) + owning client (gateway via expand_gateway_tool_ids from the catalog, no session; external by enumerating the live client), wrapping each as a FoldedMCPTool. The client object stays in the agent's tool list (Strands keeps its session alive); skill_executor runs folded tools through MCPClient.call_tool_sync. - SkillAgent._bind_mcp_tools wires it after the existing local binding. 2. Read-reference-file progressive-disclosure level. skill_dispatcher's previously-unused `reference` arg now serves a skill's supporting reference files from the PR-4 S3 SkillResourceStore on demand; the no-arg dispatch response lists available filenames so the model knows what it can read (mirrors how a real SKILL.md body names its files). 3. Bootstrap example-skill seed. seed_example_skills seeds one demonstrable bundle: instructions + a bound local tool (fetch_url_content) + an S3-uploaded reference file, granted to the default role. Mirrors seed_default_tools; the skill-resources bucket name is derived in seed.sh from the project prefix. Spec: docs/specs/admin-skills-rbac-tool-binding.md (§0.5, §8, §12). Full backend suite: 3711 passed, 3 skipped. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tools): scoped tool-id helpers + agent runtime resolution
A bare catalog tool id still means the whole MCP server; a scoped `toolId::name` selects one tool of it. Adds apis/shared/tools/scoped_ids.py as the single source of truth and resolves scoped ids at the runtime seam: the tool-filter classifies by base id, expand_gateway_tool_ids emits only the chosen gateway_<target>___<name>, and external MCP clients pass the SDK-native tool_filters allow-list (matched on the raw mcp tool name).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tools): validate and persist per-tool selections
Skill bound_tool_ids and user tool preferences accept scoped ids: the base catalog tool must exist + be active, and (when the server has a curated list) the named tool must be one it exposes. GET /tools/ now surfaces each MCP server's tools via UserToolAccess.serverTools, each with its effective enabled state (scoped pref -> server-level pref -> catalog default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tools): live discover-tools endpoints for saved MCP servers
Adds POST /admin/tools/{id}/discover (admin, skills picker) and POST /tools/{id}/discover (session auth + RBAC, model settings) so per-tool selection works for servers whose tools aren't enumerated in the catalog. External servers are listed live (OAuth-3LO falls back to the curated list); gateway targets return the tools recorded at registration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(admin): per-tool selection in the skills tool picker
The Bind Tools dialog now expands an MCP server into its individual tools with per-tool checkboxes (parent shows all/partial/none), emitting scoped ids in boundToolIds; servers with no curated list get a live Discover tools action. Bound-tool chips render as "Server · tool". Adds the frontend scoped-tool-id helper mirroring the backend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(settings): per-tool toggles in model settings
An MCP server row in model settings expands into per-tool switches; toggling the server toggles all, toggling a tool switches the server to a subset. enabledToolIds emits scoped ids for a partial server and the bare id when all tools are on. Servers with no enumerated tools offer a live Discover tools action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final increment of the admin-managed Skills feature. Stacked on PR-6b (#468). The server now defaults a user turn to the SkillAgent instead of ChatAgent (routes.DEFAULT_AGENT_TYPE = "skill"). A user with zero accessible skills gets a SkillAgent that degrades to plain ChatAgent behavior, so the flip is a no-op for them; a user whose roles grant a skill gets that skill's bound tools folded behind the two meta-tools (PR-6a/6b). Clients opt out per turn with agent_type="chat". Implementation: - routes.py: DEFAULT_AGENT_TYPE constant; the invocations() handler computes effective_agent_type = input_data.agent_type or DEFAULT_AGENT_TYPE and uses it for skill resolution + the three non-resume get_agent calls. - The resume get_agent call gates accessible_skill_ids on the snapshot's type (snapshot.agent_type == "skill"), so a turn explicitly built as "chat" that pauses on an interrupt rebuilds the SAME cache key on resume (empty skills_hash) instead of orphaning the paused agent. - service.get_agent keeps a conservative "chat" fallback for direct callers; the request-policy default lives in the route. SPA omits agent_type → gets "skill". toolTokens is measured post-deploy via the context-attribution contextBreakdown partition (folded bound tools drop out of the tools partition; the skill catalog moves into the system partition). Full backend suite: 3712 passed, 3 skipped (1 pre-existing flaky PBT test). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Uncomment the cache_config block in ModelConfig.to_bedrock_config() so caching_enabled=True now emits CacheConfig(strategy="auto"). Strands places cache points per-model and no-ops with a warning for models that don't support automatic caching, so this is safe to set unconditionally on the Bedrock path. The prior deferral was for the strands PR #1438 blocker (cachePoint blocks colliding with non-PDF document attachments), resolved in strands-agents 1.39.0 — we pin 1.40.0. Update the test that asserted the key was omitted to assert CacheConfig(strategy="auto") is present. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-referenced the env vars set by the CDK constructs (per tests/supply_chain/test_env_var_contract.py) against what the Python code reads, and added the gap to backend/src/.env.example with placeholder values and explanatory comments matching the existing style. Added (17 vars): - Core wiring: PROJECT_PREFIX, AGENTCORE_RUNTIME_WORKLOAD_NAME, INFERENCE_API_URL, LOG_LEVEL - AgentCore: MEMORY_ARN, AGENTCORE_MEMORY_TYPE, BROWSER_ID, AGENTCORE_LOCAL_OAUTH_CALLBACK_URL - DynamoDB: DYNAMODB_API_KEYS_TABLE_NAME, DYNAMODB_USER_MENU_LINKS_TABLE_NAME - New "Admin-managed Skills" section: S3_SKILL_RESOURCES_BUCKET_NAME - New "Fine-tuning" section: FINE_TUNING_ENABLED, DYNAMODB_FINE_TUNING_ACCESS_TABLE_NAME, DYNAMODB_FINE_TUNING_JOBS_TABLE_NAME, S3_FINE_TUNING_BUCKET_NAME, FINE_TUNING_DEFAULT_QUOTA_HOURS, SAGEMAKER_EXECUTION_ROLE_ARN, SAGEMAKER_SECURITY_GROUP_ID, SAGEMAKER_SUBNET_IDS Intentionally excluded: render-Lambda-internal aliases (ARTIFACTS_BUCKET, ARTIFACTS_TABLE, RENDER_TOKEN_SECRET_ARN, CSP_SCRIPT_SRC, FRAME_ANCESTOR_ORIGIN), the AWS-injected AWS_DEFAULT_REGION, and legacy local-only cruft that nothing reads. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…R-1) (#473) Adds the admin-managed chat-mode policy that the skills-mode feature (docs/specs/skills-mode.md) builds on: which agent mode (skill/chat) new conversations default to, and whether users may toggle between modes. - New apis/shared/platform_settings/ domain — ChatModeSettings stored as a SYSTEM_SETTINGS#chat-mode sentinel item in the auth-providers table (the existing first-boot convention; zero CDK changes since both app-api and the inference runtime already have the table env + IAM), with a 60s TTL-cached service for the per-turn inference read. - GET/PUT /admin/settings/chat (require_admin) with audit stamping. - GET /system/chat-settings — SPA-facing policy read (session auth). - Defaults reproduce current behavior (skill default, toggling allowed), so an unconfigured environment sees no change. Nothing consumes the policy yet — enforcement lands in PR-2. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…lls-mode PR-2) (#474) Backend half of skills mode (docs/specs/skills-mode.md): users get a listable, per-skill-toggleable view of their RBAC-granted skills, and the admin chat-mode policy from PR-1 (#473) is now enforced on /invocations. - New GET /skills/ + PUT /skills/preferences (session auth) — ACTIVE RBAC-accessible skills with per-user prefs merged, mirroring the tools endpoints. Prefs stored as USER#{id}/SKILL_PREFERENCES in the skills table (UserSkillPreference, mirrors UserToolPreference). - Shared resolver apis/shared/skills/access.py — single source of truth for "which skills does this user get" used by both app-api and the inference path (the routes helper stays as a thin test seam). - InvocationRequest.enabled_skills — None = all accessible (back-compat); a list is intersected server-side with the RBAC set (narrow, never grant); the effective set feeds SkillAgent AND the skills_hash cache key, so toggle changes can't cross-pollute cached agents. - Chat-mode policy enforcement: effective agent_type now resolves through the admin settings (TTL-cached). When toggling is disabled the client's skill/chat choice is overridden server-side; voice and other internal types bypass the policy. DEFAULT_AGENT_TYPE is now aliased to the policy model's compiled-in default so they can't drift. - PausedTurnSnapshot.enabled_skills — paused skill turns persist their effective skill set so resume rebuilds the same cache key even if the user toggles skills mid-pause; legacy snapshots fall back to request-time resolution. Full backend suite: 3808 passed, 3 skipped. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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 Sonnet 4.6 <noreply@anthropic.com>
…g (skills-mode PR-3) (#476) * feat(spa): skills mode UX — mode toggle, skills picker, request wiring (skills-mode PR-3) The user-facing half of skills mode (docs/specs/skills-mode.md), on top of the #474 backend: - New SkillService (mirrors ToolService): loads GET /skills/, per-skill optimistic toggles persisted via PUT /skills/preferences. - New ChatModeService: admin policy from GET /system/chat-settings (compiled-in default on failure), effective mode precedence = policy lock > local selection > user preferred mode > admin default. Toggling persists preferredAgentMode (user settings) and the session's agentType so a conversation reopens in the mode it was using. - Model-settings panel: Skills/Tools segmented control (hidden when the admin disallows toggling), Skills toggle section (visual twin of the Tools section, with empty state) in skills mode, Tools section in tools mode. - Chat request: sends agent_type each turn; skills mode sends enabled_skills and enabled_tools=[] (capabilities come from skills); assistant turns are excluded and keep pre-skills-mode behavior. - Session page hydrates the stored mode from session preferences, mirroring the lastModel pattern. - Backend (additive): SessionPreferences.agent_type (validated to skill|chat, merge-safe in the metadata PUT) and user settings preferredAgentMode. Frontend: 1215 tests green (ng test) + production build clean. Backend: full suite green (two pre-existing PBT tests flaked under load in an unrelated area — session-manager truncation — and pass clean in isolation and on file re-run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spa): silence the error dialog for the mode toggle's best-effort persist Toggling skills/tools mode persists preferredAgentMode via PUT /users/me/settings, which fails loud (503, #161 behavior) when user-settings storage isn't configured. The toggle already applied in-memory, so the background persist now opts out of the global error toast via the existing SUPPRESS_ERROR_TOAST context token; explicit settings-page saves stay loud. Failure still logs to console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…MCP tools (#477) In skills mode a skill-bound external MCP tool executes through the skill_executor meta-tool, so OAuthConsentHook's provider_lookup (keyed on the selected tool being an MCPAgentTool) resolved nothing and the consent gate silently never fired: the tool ran tokenless, returned an auth error as text, and the model apologized instead of the turn pausing with an oauth_required interrupt. - OAuthConsentHook: optional tool_use_provider_lookup second-chance resolver (gate + 401-retry handler) consulted when provider_lookup returns None - mcp_binding: make_folded_tool_provider_lookup maps the executor's tool_use input (skill_name/tool_name) -> bound FoldedMCPTool -> owning client -> provider id; gateway clients resolve to None (SigV4, not user OAuth) - FoldedMCPTool.invoke now returns a ToolResult-shaped error on failure so the error status survives the fold and the consent hook's 401-retry heuristic (gated on status == "error") can fire for folded tools - SkillAgent wires the resolver over its registry via the new BaseAgent._build_tool_use_provider_lookup hook point (None for chat) The resume path is unchanged: the interrupt is raised on the skill_executor call itself, so consent -> interrupt_responses -> cache warm -> the client's lazy token provider picks up the token. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… prompt (#478) An admin's needs_approval=True flag was silently bypassed in skills mode: the flagged tool runs behind the skill_executor meta-tool, so the approval hook's selected_tool lookup resolved nothing and the tool_use name never matched. Mirror the OAuth consent fold fix — an optional tool_use-based second-chance lookup resolves the folded target via the SkillRegistry, checks it against the owning client's needs_approval set, and raises the same tool_approval_required interrupt with the inner tool's name + args so the dialog describes the real tool, not the executor. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(models): add Bedrock Mantle as a model provider Mantle is AWS's OpenAI-compatible inference surface for Bedrock-hosted open-weight models (qwen, gpt-oss, gemma, deepseek, ...). It is a distinct provider from `bedrock` because it speaks the OpenAI wire protocol with a short-term bearer token rather than the Converse API with SigV4. What this adds: - apis/shared/bedrock/bearer_token.py: SigV4-presigned `CallWithBearerToken` bearer token (ported inline from aws-bedrock-token-generator, no new dep) plus region+path base-URL helper. - GET /admin/mantle/models: browse the live regional Mantle roster via the OpenAI SDK (seeds the escape-hatch form's model-id suggestions). - ModelProvider.MANTLE end to end: model_config translation, agent_factory builds a Strands OpenAIModel against the Mantle base URL, BaseAgent + inference chat pipeline thread the value through. - mantle_endpoint_path on the managed-model shape, persisted and resolved server-authoritatively in _resolve_model_settings. Mantle serves different models on different paths (/v1 vs /openai/v1) and exposes NO API to discover which, so the path is recorded per model (from the model card) rather than probed or mapped. Carried through the paused-turn snapshot so a resumed Mantle turn rebuilds the same base URL. Caching is intentionally left off for Mantle: prompt caching on Bedrock is model-bound to Anthropic Claude + a few Amazon Nova models, none of which run through the Mantle provider. Requires `bedrock-mantle:*` IAM (separate infra commit) and Mantle being enabled for the account/region. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(infra): grant bedrock-mantle IAM for Mantle browse + inference Bedrock Mantle has its own IAM service namespace — `bedrock-mantle:*`, NOT `bedrock:*`. Mirror the AWS-managed AmazonBedrockMantleInferenceAccess policy: - AgentCore runtime role: `bedrock-mantle:CreateInference` + `Get*`/`List*` on `project/*`, plus `bedrock-mantle:CallWithBearerToken` (mantle-provider inference). - App-API task role: read-only `Get*`/`List*` + `CallWithBearerToken` for the GET /admin/mantle/models browse endpoint. The token signer is authorized against this namespace; without it inference returns an IAM denial even when Mantle is enabled for the account. Integration test asserts both roles carry CallWithBearerToken and the runtime carries CreateInference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(spa): curated Bedrock Mantle catalog + endpoint-path form Add a "Bedrock Mantle" tab to the admin model catalog, mirroring the Bedrock tab: curated cards with vetted, human-reviewed settings (pricing, modalities, context, and the all-important endpoint path baked in). No probing, no magic. - CURATED_MANTLE_MODELS seeded with Qwen3 Coder 30B (/v1) and Gemma 4 31B (/openai/v1). Pricing verified against the AWS Bedrock pricing page (2026-06); modalities/capabilities/context/path verified against each model card (Gemma 4: text+image+video in, text out, reasoning + tool use + vision). - Model shape gains `mantleEndpointPath`; the model form becomes Mantle-aware: a /v1 vs /openai/v1 selector (with model-card guidance), caching controls hidden (Mantle open-weight models don't cache), and a model-id datalist seeded from the live GET /admin/mantle/models roster for off-catalog adds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /users/me/sync now derives the persisted email from
current_user.email (the JWT-bound value populated by the BFF callback
at login time) instead of the request body. Display fields (name,
picture) still come from the body; the field set on
UserProfileSyncRequest is reduced to those plus provider_sub.
Pydantic's extra='allow' setting keeps the endpoint backwards-
compatible with clients that still send legacy keys ('roles', 'email')
— they surface via model_extra and are dropped when building the
persisted profile. The handler logs a single WARNING listing every
legacy field it observed so stale clients can be tracked down.
A session whose JWT has no email claim now returns 422 rather than
persisting an empty row.
Tests:
- test_sync_persists_jwt_email_not_body_email: a body-supplied email
does not influence the persisted record.
- test_sync_email_domain_derived_from_jwt: the derived email_domain
also tracks the JWT, not the body.
- test_sync_jwt_email_normalized_to_lowercase: persisted email is
lowercased regardless of the JWT casing.
- test_sync_missing_jwt_email_returns_422: missing JWT email refuses
the upsert.
- test_sync_warns_when_legacy_fields_present: combined warn covers
both 'roles' and 'email' in body.
- test_share_access_email_match (7 tests): locks in that
ShareService._check_access compares the share's allowed_emails list
against requester.email only — case-insensitive, with owner override
and access_level=public/specific semantics covered.
SystemPromptBuilder.from_user_prompt now assembles every prompt as:
PLATFORM_SAFETY_FLOOR
<user_instructions>
<user-supplied text>
</user_instructions>
The floor states that text inside the user_instructions tag is advisory,
that tool-input policies are enforced server-side and cannot be coerced,
that the code-execution surface accepts only chart / dataframe code, and
that identity claims come from the validated session rather than the
prompt. Embedded user_instructions tags in the user portion are stripped
before assembly so a caller cannot close the wrapper and write text below
the floor.
User-supplied prompts are bounded:
- SystemPromptBuilder truncates the user portion to MAX_USER_PROMPT_LENGTH
(8 KiB) before assembly.
- InvocationRequest.system_prompt rejects payloads larger than
MAX_USER_SYSTEM_PROMPT_CHARS (also 8 KiB) at the API boundary so
oversized inputs surface as a 4xx instead of being silently truncated.
The diagram-tool input policy added in apis/shared/security/python_ast_policy.py
remains the authoritative enforcement against arbitrary code execution;
this branch is structural defense-in-depth at the prompt boundary.
Tests:
- test_system_prompt_safety_floor.py (10 new tests) covers floor
precedence over user portion, behaviour for empty/None user prompts,
resistance to wrapper-tag injection (closing or opening), length
truncation, exact-limit pass-through, and the API request boundary's
oversize rejection / valid-size acceptance / None-allowed contracts.
- test_system_prompt_builder.py existing TestFromUserPrompt updated
from 'user prompt becomes the entire base prompt unchanged' to
'assembled prompt starts with the floor and contains the user text
inside the wrapper tags'.
The fetch_url_content tool now runs every URL through the apis.shared.security.url_validator helper before opening any HTTP connection. The validator rejects loopback, link-local, RFC1918, ULA, multicast, reserved, unspecified, CGNAT, and cloud metadata-service addresses, and resolves every DNS answer to defeat host-name rebinding to a private result. Redirect handling is now manual. The httpx client is constructed with follow_redirects=False; the tool walks the redirect chain itself, applying the same validator to each Location target before re-fetching. Up to three redirects are followed; a 3xx without a Location header falls through to the standard response path. Redirect targets that fail the validator surface a generic 'Redirect target is not permitted.' error to the caller. Tests: - 8-case parametrized rejection sweep covering 169.254.169.254, fd00:ec2::254, 127.0.0.1, RFC1918 ranges, [::1], and 0.0.0.0, asserting that AsyncClient is never constructed when validation fails (ergo no network I/O). - DNS-resolution rebinding: a host name whose only A record is private is rejected with no client construction. - Disallowed scheme (file://) rejected without network I/O. - Generic error contract: rejection message contains no resolved address or 'metadata' label. - Public URL passes the validator and reaches the network layer. - Redirect handling: the constructed client must use follow_redirects=False, and a 302 to 169.254.169.254 results in exactly one outbound request (the original) — the tool never follows to the metadata IP.
The PUT /sessions/{session_id}/metadata handler now consults a new
session_exists_for_other_user() helper before taking the
'session-not-found-for-this-user → create new' branch. The helper
queries the SessionLookupIndex GSI directly (without filtering on
userId) and reports whether a row exists for the given session id
under any user. When that returns True for a non-owner, the PUT
returns 404 — matching GET's behaviour and avoiding an enumeration
oracle.
The existing per-user fetch (get_session_metadata) already returns
None for non-owners because of how its DynamoDB query is partitioned;
the new helper closes the gap that None has two distinct meanings:
genuinely empty vs. taken-by-someone-else.
Tests:
- TestUpdateSessionMetadataOwnership (4 tests):
- 404 when the session id is taken under a different user, and the
write helper is never called.
- The existence-check verdict alone is enough to block the write
even when the per-user fetch returned None.
- Genuinely fresh session ids still create normally.
- Owner updates skip the existence check entirely (the per-user
fetch already returned the record).
- Two existing 'create-new' tests updated to mock the new helper.
detect_aws_service_from_url now returns Optional[str]: a recognized
service name for Lambda Function URLs, API Gateway, and AgentCore
Gateway hosts, and None for everything else. The previous fallback to
'lambda' for unrecognized hosts meant the SigV4 signer would attach
task IAM credentials (Authorization: AWS4-HMAC-SHA256 ... +
X-Amz-Security-Token) to outbound requests destined for arbitrary
hosts.
create_external_mcp_client treats the None return as a refusal: when
auth_type=aws-iam and the server URL doesn't match a known AWS
service, the function returns None instead of constructing a
SigV4-wired client. The /admin/tools/discover route already maps a
None client to a 400 with a generic message, so the discovery
endpoint's contract for arbitrary-URL admin discovery still holds for
non-signing auth modes (auth_type=none / oauth2 / etc.) — only the
credential-bearing AWS IAM path is scoped down.
Tests:
- test_mcp_sigv4_scope.py (14 tests):
- 4 service-detection cases for legitimate AWS hostnames
(Lambda Function URL, API Gateway, AgentCore Gateway).
- 7 None-return cases for non-AWS hosts including hostname
look-alikes (amazonaws.com.attacker.com, lambda-url.fake.com,
lambda-url-lookalike.example.com).
- aws-iam against a non-AWS URL refuses client construction.
- aws-iam against an AWS URL constructs normally.
- Non-signing auth modes (auth_type=none) against arbitrary URLs
still construct, since no credentials are at stake.
- Existing test_defaults_to_lambda_for_unknown_url replaced with
test_returns_none_for_unknown_url to reflect the new contract.
) The assistants RAG knowledge base is backed by an AWS::S3Vectors::Index ('rag-vector-index-v1') that lives in a separate AWS service from regular S3 — the s3vectors boto3 client / AWS::S3Vectors::* CFN types. 'aws s3 sync' cannot reach it, list_objects_v2 doesn't see it, and nothing in backup.py or restore.py was touching it. The result: after teardown -> redeploy -> restore, the new vector index was empty, every assistant's knowledge base appeared connected (DDB document metadata restored, S3 originals restored) but every retrieval call returned zero hits because no vectors existed for any assistant_id. This change closes that gap with the same 'snapshot and replay' model the rest of the restore tool uses. backup.py - new VECTOR_INDEXES list (parallel to S3_BUCKETS) - new backup_vector_index() that paginates s3vectors.list_vectors with returnData=True + returnMetadata=True and streams every record to vectors/{logical}.jsonl.gz in the backup bucket. The line format ({key, data, metadata}) is byte-compatible with s3vectors.put_vectors on restore. - run() iterates VECTOR_INDEXES after the regular S3 buckets pass restore.py - new VECTOR_INDEXES constant mirroring backup.py - new restore_vector_index() that reads the gzipped JSONL, batches records 50-at-a-time (matching bedrock_embeddings.store_embeddings _in_s3 BATCH_SIZE), and calls s3vectors.put_vectors. Idempotent on re-run (put_vectors with same key is upsert), skips cleanly on older backups that pre-date the vectors snapshot, and skips cleanly on target prefixes where RAG is disabled. - run_restore() runs the vector restore step after the S3 buckets pass and before the AgentCore Memory replay tests - tests/supply_chain/test_backup_coverage.py adds TestBackupCoversVectorIndexes — 5 assertions that scan the CDK constructs for AWS::S3Vectors::Index resources and verify backup.py declares them, calls list_vectors with returnData/returnMetadata, and restore.py both defines AND wires in restore_vector_index. Same canary pattern that already covers DynamoDB tables and regular S3 buckets. - tests/supply_chain/test_backup_coverage.py: extract_backup_tables is now section-scoped so its 'logical' regex stops slurping entries from S3_BUCKETS or the new VECTOR_INDEXES. Drive-by fix. - scripts/restore-data/test_restore.py adds 7 behavioral tests for restore_vector_index covering: 1:1 round-trip with batch-of-50 flushing, missing backup file skip, missing target SSM skip, dry-run, idempotent re-run, unknown logical name skip, and the rag-vectors logical-name pin. All 48/48 restore-data tests pass; all 5/5 new vector coverage tests pass. The 3 pre-existing supply-chain failures (system-prompts table and skill-resources bucket from prior feature PRs) are untouched and unrelated. NOTE: the existing affected forker still has an empty vector index post-restore. This PR does not include a one-off recovery script — that's intentionally scoped as a separate task per the diagnosis discussion. Co-authored-by: Colin <colin@boisestate.edu>
Adds a standalone, directly-navigable maintenance splash at /maintenance for use during migration downtime. It lives in src/pages/ (outside Starlight's content collection), so it never appears in the sidebar/nav and renders without Starlight chrome — reachable only by navigating to the URL directly. Self-contained markup + styles (works even if nothing else loads) matching the docs/SPA house style: Boise blue, frosted glass, lava-lamp blob field, graph-paper grid, Bricolage display type, a pulsing Bronco-orange status orb, and an indeterminate progress shimmer. Responsive, honors prefers-reduced-motion, and carries noindex/nofollow so crawlers skip it. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The maintenance splash (added in #482) is purely informational, so the "Try again" reload button and "Status & updates" link add no real value on a static page. Drop both buttons along with the now- unused click handler script and the .actions/.btn CSS, leaving the status pill, headline, message, progress shimmer, and sign-off. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A handful of related cleanups across admin and data-edge routes that
share a common shape: each one either accepted ill-formed input that
went on to crash a downstream call, leaked upstream error detail in
500 bodies, or didn't verify ownership before mutating shared records.
Admin/Bedrock model listing
GET /admin/bedrock/models filter parameters now use Pydantic
Literal/regex types so out-of-shape values are refused at the
request boundary with a generic 422 (no input echo, no enum-set
reflection). The route's bespoke ClientError/BotoCoreError/Exception
handlers are removed in favour of the app-wide
register_aws_client_error_handler installed at startup, which maps
ValidationException-class codes to a generic 400 and other
ClientErrors to a generic 502. A new register_validation_error_handler
replaces FastAPI's default 422 body with the same generic body.
Memory-record delete
DELETE /memory/{record_id} now fetches the record's metadata via
GetMemoryRecord and verifies the calling user appears in the
/actors/{user_id} segment of the record's namespace before issuing
batch_delete_memory_records. Non-owners (and missing records) get a
generic 404. Two new helpers — get_memory_record_owner and
record_namespace_owner — encapsulate the lookup and namespace parse.
OIDC discovery + auth-provider connectivity tests
POST /admin/auth-providers/discover now runs the supplied issuer URL
through the SSRF validator (https-only, no loopback / link-local /
private / metadata addresses) before fetching .well-known/openid-
configuration. POST /admin/auth-providers/{id}/test now applies the
same validator to each stored URL — jwks_uri, token_endpoint, and
the issuer-derived discovery URL — independently; a URL that fails
validation is reported unreachable in the per-endpoint result map
and is not contacted.
Files pagination cursor
GET /files now refuses cursors whose embedded PK doesn't match the
caller's USER#{user_id} partition. The repository raises a new
InvalidCursorError; the route maps that to a generic 400 instead of
the previous 500 the cross-partition DynamoDB query produced.
Tests:
- test_admin_bedrock_models.py (16) — Pydantic rejection of bad
enum/regex filters short-circuits before any boto3 call; AWS
ValidationException maps to 400 with no message reflection; other
ClientError maps to 502 generic; legitimate filters reach AWS;
unhandled exception → 500 with no exception detail in body.
- test_memory_delete_ownership.py (4) — owner can delete; non-owner
gets 404 and no AWS call; record without namespaces treated as not
owned; missing record → 404 with no AWS call.
- test_auth_providers_ssrf.py (12) — discover refuses 8 disallowed
issuer shapes (loopback, link-local, RFC1918, IPv6 loopback, http://,
file://, javascript:); accepts a legitimate https issuer; test
endpoint skips disallowed jwks_uri / token_endpoint / issuer URLs
individually, marking them unreachable without contacting them.
- test_files_cursor_validation.py (5) — cursor with another user's
PK rejected at repository, garbage cursor rejected, cursor without
PK rejected, owner's own cursor still works, route maps the error
to 400.
- Two existing auth_providers tests updated to mock DNS through the
validator's getaddrinfo so their fake hostnames continue to work.
…oy workflows (#485) The artifacts iframe CSP frame-ancestors list is built from the deployed SPA origin plus config.artifacts.extraFrameAncestors (fed by CDK_ARTIFACTS_EXTRA_FRAME_ANCESTORS), and is enforced on both the artifacts CloudFront response-headers-policy and the render Lambda's FRAME_ANCESTOR_ORIGIN. But neither platform.yml nor nightly-deploy-pipeline.yml passed that var through, so there was no way to allow a local SPA (http://localhost:4200) to embed artifacts served by a deployed dev distribution — the iframe fails with "refused to connect". Mirror the existing CDK_MCP_SANDBOX_EXTRA_FRAME_ANCESTORS passthrough. Unset by default (config falls back to []), so prod is a no-op. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Three small, independent cleanups: Fine-tuning jobs listing — tolerant deserialization GET /admin/fine-tuning/jobs now serializes records one at a time; any record that fails JobResponse validation is dropped with a WARN log naming the offending job_id, and the listing returns 200 with the well-formed records. Repository-level failures (the table unreachable, etc.) still surface as 500 — only per-record validation drops are tolerated. External model listing — generic 503 on missing credentials GET /admin/gemini/models and GET /admin/openai/models now respond with 503 'External model provider not configured.' when the required API key environment variable is unset. The specific env var name (GOOGLE_API_KEY / GOOGLE_GEMINI_API_KEY / OPENAI_API_KEY) is logged server-side at ERROR for operator diagnostics, never echoed to the response body. Transport security baseline CloudFront SPA distribution now pins MinimumProtocolVersion to TLSv1.2_2021 when serving a custom domain — drops TLS 1.0/1.1 entirely and prunes the cipher set to AEAD-only. ALB HTTPS listener now pins SslPolicy to ELBSecurityPolicy-TLS13-1- 2-Res-2021-06 — TLS 1.2 minimum, modern ciphers only, no CBC. Tests: - test_admin_lows.py (8) — fine-tuning listing skips malformed records and returns 200; well-formed-only and all-malformed cases; repository failure still surfaces as 500; gemini and openai unconfigured each return 503 with no env-var-name reflection in the body. - transport-security.test.ts (2) — CDK synth assertions that the CloudFront distribution sets MinimumProtocolVersion=TLSv1.2_2021 and the ALB HTTPS listener sets a 2021-vintage SslPolicy.
…ings (#486) * fix(skills): resolve subset-scoped external MCP bindings at fold time A skill binding a *subset* of an external MCP server (scoped ids like `canvas::courses`) resolved to no client at fold time, so the skill folded zero tools and the model reported the server "not connected" — the OAuth consent gate never fired because no FoldedMCPTool existed. - base_agent: register/look up external tools by their *base* catalog id (the catalog and tool filter both key on the base), deduping scoped ids for the same server so a per-tool binding is classified and loaded. - ExternalMCPIntegration.get_client: collapse a scoped lookup id to its base and match the cached client, tolerating the `|allow:<names>` subset cache-key suffix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(skills): reset stale tool folds when rebuilding skill agents MCP clients are process-global and reused across agent builds, and the fold set persists on them (set_folded_tool_names only ever adds). resolve_mcp_bindings enumerates an external server through that same fold-filtered list_tools_sync, so a stale fold from a prior build makes a re-bind see zero tools — the bound tool "works once, then disappears" on the next turn. - mcp_tool_folding: add reset_folded_tool_names to clear a client's fold set and cached tool list. - skill_agent: reset every client this build will (re)bind before resolving, then let the build recompute and re-apply the fold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps vulnerable dependencies across backend, scripts, and frontend to patched versions. All 22 open HIGH-severity Dependabot alerts addressed. Backend (pyproject.toml + uv.lock): - cryptography 47.0.0 -> 48.0.1 (GHSA-537c-gmf6-5ccf) - starlette 1.0.0 -> 1.3.1 (CVE-2026-48818, CVE-2026-54283) - python-multipart 0.0.27 -> 0.0.30 (CVE-2026-53539) - pyjwt[crypto] 2.12.1 -> 2.13.0 (CVE-2026-48526) - urllib3 pinned 2.7.0 (CVE-2026-44431, CVE-2026-44432) Scripts (backup-data, restore-data): - add uv constraint cryptography>=48.0.1 -> 49.0.0 (GHSA-537c-gmf6-5ccf) Frontend (package.json + package-lock.json): - @angular/* framework 21.2.11 -> 21.2.17 (CVE-2026-50170/50171/54266/54267/54268) (cdk 21.2.14, build/cli 21.2.16 -- latest existing patch for those packages) - hono override -> 4.12.26 (CVE-2026-54290) - piscina override -> 5.2.0 (CVE-2026-55388) - undici override -> 7.28.0 (CVE-2026-9697) - vite override -> 8.0.16 (CVE-2026-53571) Verified: backend pytest 3935 passed/3 skipped; frontend build OK; frontend unit tests 1216 passed. All locked versions >= patched thresholds.
* fix(deps): remediate all 22 HIGH Dependabot findings Bumps vulnerable dependencies across backend, scripts, and frontend to patched versions. All 22 open HIGH-severity Dependabot alerts addressed. Backend (pyproject.toml + uv.lock): - cryptography 47.0.0 -> 48.0.1 (GHSA-537c-gmf6-5ccf) - starlette 1.0.0 -> 1.3.1 (CVE-2026-48818, CVE-2026-54283) - python-multipart 0.0.27 -> 0.0.30 (CVE-2026-53539) - pyjwt[crypto] 2.12.1 -> 2.13.0 (CVE-2026-48526) - urllib3 pinned 2.7.0 (CVE-2026-44431, CVE-2026-44432) Scripts (backup-data, restore-data): - add uv constraint cryptography>=48.0.1 -> 49.0.0 (GHSA-537c-gmf6-5ccf) Frontend (package.json + package-lock.json): - @angular/* framework 21.2.11 -> 21.2.17 (CVE-2026-50170/50171/54266/54267/54268) (cdk 21.2.14, build/cli 21.2.16 -- latest existing patch for those packages) - hono override -> 4.12.26 (CVE-2026-54290) - piscina override -> 5.2.0 (CVE-2026-55388) - undici override -> 7.28.0 (CVE-2026-9697) - vite override -> 8.0.16 (CVE-2026-53571) Verified: backend pytest 3935 passed/3 skipped; frontend build OK; frontend unit tests 1216 passed. All locked versions >= patched thresholds. * fix(deps): remediate easy MEDIUM/LOW Dependabot findings Straightforward in-range/direct dependency bumps for remaining alerts. Risky or blocked findings deliberately left out (see below). Backend (pyproject.toml + uv.lock): - aiohttp 3.13.5 -> 3.14.1 - authlib 1.7.0 -> 1.7.1 - python-multipart 0.0.30 -> 0.0.31 - idna pinned 3.15 (transitive) Scripts (backup-data, restore-data): - pytest 8.4.2 -> 9.0.3 (dev) Frontend (package.json + package-lock.json): - mermaid 11.14.0 -> 11.15.0 (direct) - @babel/core override ">=7.29.6 <8.0.0" -> 7.29.7 (bounded to 7.x; open-ended pulled babel 8 which broke the Angular build) Infrastructure (package-lock.json, lock-only): - @babel/core -> 7.29.7 (within range) Deliberately NOT included (not "easy"): - esbuild 0.28.1: pinned by vite 8; override risks build breakage (low sev) - js-yaml 4.2.0: major bump (consumers pin ^3) - dompurify: one alert has no patched version published - infra fast-uri (HIGH) / brace-expansion / js-yaml: bundled inside aws-cdk-lib@2.251.0/node_modules; npm overrides can't reach them. Requires an aws-cdk-lib upgrade -- separate, deliberate change. Verified: backend pytest 3935 passed/3 skipped; frontend build OK + 1216 unit tests passed; infra tsc build OK + 396 jest tests passed.
* fix(deps): remediate all 22 HIGH Dependabot findings Bumps vulnerable dependencies across backend, scripts, and frontend to patched versions. All 22 open HIGH-severity Dependabot alerts addressed. Backend (pyproject.toml + uv.lock): - cryptography 47.0.0 -> 48.0.1 (GHSA-537c-gmf6-5ccf) - starlette 1.0.0 -> 1.3.1 (CVE-2026-48818, CVE-2026-54283) - python-multipart 0.0.27 -> 0.0.30 (CVE-2026-53539) - pyjwt[crypto] 2.12.1 -> 2.13.0 (CVE-2026-48526) - urllib3 pinned 2.7.0 (CVE-2026-44431, CVE-2026-44432) Scripts (backup-data, restore-data): - add uv constraint cryptography>=48.0.1 -> 49.0.0 (GHSA-537c-gmf6-5ccf) Frontend (package.json + package-lock.json): - @angular/* framework 21.2.11 -> 21.2.17 (CVE-2026-50170/50171/54266/54267/54268) (cdk 21.2.14, build/cli 21.2.16 -- latest existing patch for those packages) - hono override -> 4.12.26 (CVE-2026-54290) - piscina override -> 5.2.0 (CVE-2026-55388) - undici override -> 7.28.0 (CVE-2026-9697) - vite override -> 8.0.16 (CVE-2026-53571) Verified: backend pytest 3935 passed/3 skipped; frontend build OK; frontend unit tests 1216 passed. All locked versions >= patched thresholds. * fix(deps): remediate easy MEDIUM/LOW Dependabot findings Straightforward in-range/direct dependency bumps for remaining alerts. Risky or blocked findings deliberately left out (see below). Backend (pyproject.toml + uv.lock): - aiohttp 3.13.5 -> 3.14.1 - authlib 1.7.0 -> 1.7.1 - python-multipart 0.0.30 -> 0.0.31 - idna pinned 3.15 (transitive) Scripts (backup-data, restore-data): - pytest 8.4.2 -> 9.0.3 (dev) Frontend (package.json + package-lock.json): - mermaid 11.14.0 -> 11.15.0 (direct) - @babel/core override ">=7.29.6 <8.0.0" -> 7.29.7 (bounded to 7.x; open-ended pulled babel 8 which broke the Angular build) Infrastructure (package-lock.json, lock-only): - @babel/core -> 7.29.7 (within range) Deliberately NOT included (not "easy"): - esbuild 0.28.1: pinned by vite 8; override risks build breakage (low sev) - js-yaml 4.2.0: major bump (consumers pin ^3) - dompurify: one alert has no patched version published - infra fast-uri (HIGH) / brace-expansion / js-yaml: bundled inside aws-cdk-lib@2.251.0/node_modules; npm overrides can't reach them. Requires an aws-cdk-lib upgrade -- separate, deliberate change. Verified: backend pytest 3935 passed/3 skipped; frontend build OK + 1216 unit tests passed; infra tsc build OK + 396 jest tests passed. * fix(deps): upgrade aws-cdk-lib 2.251.0 -> 2.260.0 to clear bundled CVEs The remaining infra Dependabot HIGH/MEDIUM alerts were in deps bundled inside aws-cdk-lib's own node_modules (unreachable by npm overrides): - fast-uri 3.1.0 -> 3.1.2 (HIGH, 2 advisories) — bundled via ajv - brace-expansion 5.0.5 -> 5.0.6 (MEDIUM) — bundled via minimatch Bumping aws-cdk-lib to 2.260.0 (latest 2.x) re-bundles both at patched versions. constructs 10.6.0 already satisfies the ^10.5.0 peer (unchanged). Verified: infra tsc build clean; jest 396 passed/18 suites (full PlatformStack construction + Template.fromStack assertions, no template regressions). cdk-lib v2 minor bump, backward compatible.
The deploy workflows (backend.yml, platform.yml, frontend-deploy.yml) only run on push to develop/main and run their test jobs as a pre-deploy gate, so unit tests never executed on the PR itself. PRs into develop ran only skip-auth-guard. Adds .github/workflows/ci.yml triggered on pull_request -> [develop, main] with three parallel test jobs reusing the existing commands: - test-backend: uv sync + uv run pytest tests/ - test-frontend: npm ci + npm run test:ci (vitest) - test-infra: npm ci + npx jest No build/deploy/AWS steps — deploys stay push-only. Actions are SHA-pinned with the shared checkout SHA, runners pinned to ubuntu-24.04, and cancel-in-progress: true (safe; no CDK deploy). Conforms to all tests/supply_chain checks (31 passed).
…across edge origins (#491) Collapse the three-separate-cert first-deploy footgun into one shared wildcard, and make cert handling consistent and fail-loud across all CloudFront origins. - config.ts: add CDK_CLOUDFRONT_CERTIFICATE_ARN (top-level cloudfrontCertificateArn). frontend/artifacts/mcpSandbox certs fall back to it when their section-specific ARN is unset; section-specific wins. ALB cert stays separate (region-specific). One us-east-1 wildcard ({domain}+*.{domain}) now satisfies all three origins. - artifacts-distribution-construct: add domain-set-but-cert-missing guard mirroring mcp-sandbox (replaces the false 'config.ts already enforced' comment + opaque fromCertificateArn(undefined) crash), and add a domain-less fallback to the CloudFront default domain so domain-less synth no longer crashes with 'reading startsWith'. - load-env.sh: forward cloudfrontCertificateArn + mcpSandbox.certificateArn context params (were missing, breaking the cdk.context.json path). - workflows: wire CDK_CLOUDFRONT_CERTIFICATE_ARN job-level env in platform / nightly / teardown. - docs: step-02/step-03/ACTIONS-REFERENCE recommend the single shared cert and reframe per-origin vars as optional overrides; troubleshooting entry for the synth cert-guard failure. - tests: CloudFront cert resolution (config), artifacts cert guard + domain-less fallback, and end-to-end shared-cert PlatformStack synth. Full infra suite: 20 suites / 406 tests green.
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>
…igner-phase-4-ui-666b5e fix(agent-designer): model write-check rejects models the picker lists (403)
…ew 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>
…av-admin-gate feat(sidenav): gate Memory Spaces + Agents to system-admin, add Preview badges
…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>
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>
…ol-binding-resolution feat(topnav): surface active assistant in the top nav
… 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>
…ut (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>
Reconcile develop with main after the v1.1.0 squash-merge (#604). Adopts main's release artifacts (VERSION 1.1.0, CHANGELOG/RELEASE_NOTES 1.1.0 entries, synced manifests + lockfiles, README badge) and drops develop's now-obsolete CHANGELOG [Unreleased] block, whose Agent Designer Phase 1 content shipped in the 1.1.0 entry. All feature code auto-merged (identical on both branches).
…nto-develop-1.1.0 Backmerge/main into develop 1.1.0
…ill-binding-resolution feat(agent-designer): resolve skill bindings at invocation (replace + force skill-mode)
…at-input-lock feat(agent-designer): reflect governed agent bindings in the chat-input (lock pickers)
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>
…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>
…elease-and-filter fix(agent-designer): release picker locks on new chat + show only bound tools/skills
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>
…-default-on chore(agent-designer): default AGENTS_API_ENABLED on with a kill switch
… 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>
…rm 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>
…owledge-base feat(agent-designer): manage an agent's knowledge base from the Agent Designer
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>
…gner-params-and-preview # Conflicts: # frontend/ai.client/src/app/agents/agent-form/agent-form.page.ts
…signer-params-and-preview feat(agent-designer): governed model params + live editor preview
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>
…eview-model-lock-nav-trim feat(agent-designer): lock preview model picker; trim preview nav
Agent Designer completion: skill-binding resolution at invocation (completes the model/tool/skill trio), chat-input reflects governed agent bindings, live editor preview + model-parameter governance, full knowledge-base management in the Designer, and AGENTS_API_ENABLED flipped default-on (nav stays admin-gated Preview). Code-only; no new AWS resources and no migration. - Bump VERSION to 1.2.0; sync manifests + lockfiles - CHANGELOG.md and RELEASE_NOTES.md 1.2.0 entries (#602, #603, #606, #607, #608, #609, #611)
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.
see release notes