diff --git a/.github/workflows/nightly-deploy-pipeline.yml b/.github/workflows/nightly-deploy-pipeline.yml index 1f9778c2..b2161f6f 100644 --- a/.github/workflows/nightly-deploy-pipeline.yml +++ b/.github/workflows/nightly-deploy-pipeline.yml @@ -84,6 +84,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -105,6 +106,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: version: '0.7.12' @@ -126,6 +128,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -176,6 +179,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -212,6 +216,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -243,6 +248,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -281,6 +287,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -317,6 +324,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -348,6 +356,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -379,6 +388,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -410,6 +420,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -440,6 +451,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -475,6 +487,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} @@ -509,6 +522,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' @@ -573,6 +587,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} + persist-credentials: false - uses: ./.github/actions/configure-aws-credentials with: aws-region: ${{ vars.AWS_REGION || 'us-west-2' }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4a2766f2..f8116717 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -269,7 +269,7 @@ jobs: - name: Install frontend dependencies if: steps.cache-frontend.outputs.cache-hit != 'true' - run: bash scripts/stack-frontend/install.sh + run: bash scripts/frontend/install.sh - name: Save frontend node_modules cache if: steps.cache-frontend.outputs.cache-hit != 'true' @@ -307,7 +307,7 @@ jobs: - name: Run backend tests with coverage id: run-tests - run: bash scripts/stack-app-api/test.sh + run: bash scripts/backend/test.sh - name: Upload backend coverage artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -348,7 +348,7 @@ jobs: - name: Run frontend tests with coverage id: run-tests - run: bash scripts/stack-frontend/test.sh + run: bash scripts/frontend/test.sh - name: Upload frontend coverage artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 85261f21..b9ac5091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to this project are documented in this file. Format follows For narrative release notes written for operators and product owners, see [RELEASE_NOTES.md](RELEASE_NOTES.md). +## [1.0.2] - 2026-06-29 + +Second patch on the 1.0.0 single-stack architecture. Headlined by **restoring tool use in assistant chats** (reverting the 1.0.0 knowledge-base-only change), plus a CodeQL security-hardening sweep, remediation of 6 Dependabot alerts, and a nightly-pipeline fix. No migration; upgrade in place. + +### ✨ Improved + +- **Assistants can use tools again** β€” reverts the 1.0.0 knowledge-base-only restriction (#382). The inference API no longer forces `enabled_tools=[]` on assistant turns and the "Knowledge Base Grounding / no external tools" directive is removed from the system prompt, so the user's selected MCP/tools flow through to assistant chats again. KB context is still pre-stuffed into the user message and assistant instructions still apply. The editor preview now forwards the owner's enabled tools and renders tool-use cards, matching consumer chat. Assistant chats emit tool-use (and MCP-App) events again (#517) + +### πŸ› Fixed + +- Nightly coverage pipeline: `test-backend`, `test-frontend`, and `install-frontend` jobs failed with "No such file or directory" after the single-stack refactor removed `scripts/stack-app-api/` and `scripts/stack-frontend/`; repointed `nightly.yml` to the sanctioned `scripts/backend/test.sh`, `scripts/frontend/install.sh`, and `scripts/frontend/test.sh` (behavior preserved 1:1) (#518) + +### πŸ”’ Security + +- **HIGH `py/incomplete-url-substring-sanitization`** β€” `external_mcp_client` now parses the URL host (`urlparse`) and matches an anchored suffix instead of substring-checking the whole URL, so an AWS marker in a path/query/userinfo can no longer trick SigV4 signing into attaching IAM credentials to a non-AWS host (#521) +- **HIGH `js/regex/missing-regexp-anchor`** β€” `admin-tool.model` parses the host (`new URL`) and anchors the AWS-endpoint regexes (`$`), preventing spoofed-host matches (#521) +- **MEDIUM `py/log-injection`** (24 sites across 16 files) β€” new `apis.shared.security.scrub_log()` neutralizes CR/LF/control characters; applied to every flagged user-controlled log value (#521) +- **MEDIUM `actions/untrusted-checkout`** β€” added `persist-credentials: false` to all `inputs.ref` checkouts in `nightly-deploy-pipeline.yml` (#521) +- **WARNING `py/regex/duplicate-in-character-class`** β€” removed a stray `[` from a `re.VERBOSE` comment that the parser misread as a character class (#521) +- Added regression tests: `TestAwsUrlHostSanitization`, `admin-tool.model.spec.ts` (13 cases), `test_log_sanitize.py` (#521) + +### πŸ“¦ Dependencies + +- **docs-site:** `astro` 6.3.1 β†’ 6.4.8 (reflected XSS via slot name GHSA-8hv8-536x-4wqp, host-header SSRF GHSA-2pvr-wf23-7pc7, spread-attribute XSS GHSA-jrpj-wcv7-9fh9); `esbuild` β†’ 0.28.1 via overrides (dev-server arbitrary file read GHSA-g7r4-m6w7-qqqr) +- **frontend:** `esbuild` β†’ 0.28.1 via overrides (transitive through `@angular/build` 21.2.16; GHSA-g7r4-m6w7-qqqr) +- **backend:** `pydantic-settings` 2.13.1 β†’ 2.14.2 (transitive; `NestedSecretsSettingsSource` symlink traversal / local file read GHSA-4xgf-cpjx-pc3j) (#520) + ## [1.0.1] - 2026-06-26 First patch on top of the 1.0.0 general-availability release. Adds the ability to **save a conversation to a connected app** ("Save to…", with Google Drive as the reference export target) and **support for external (cross-account) Route53 hosted zones**. Both additions are additive and off by default until configured; existing 1.0.0 deployments upgrade in place with no migration. diff --git a/README.md b/README.md index 2e1a0a8e..129dbd34 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **An open-source, production-ready Generative AI platform for institutions** *Built by Boise State University, designed for everyone.* -[![Release](https://img.shields.io/badge/Release-v1.0.1-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) +[![Release](https://img.shields.io/badge/Release-v1.0.2-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) [![Nightly](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml/badge.svg)](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml) ![Python](https://img.shields.io/badge/Python-3.13+-3776AB?style=flat&logo=python&logoColor=white) @@ -296,7 +296,7 @@ agentcore-public-stack/ See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full changelog, including new features, bug fixes, platform upgrades, and deployment notes for each release. -**Current release:** v1.0.1 +**Current release:** v1.0.2 --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 67707454..2bb7ab01 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,73 @@ +# Release Notes β€” v1.0.2 + +**Release Date:** June 29, 2026 +**Previous Release:** v1.0.1 (June 26, 2026) + +--- + +> ⚠️ **Coming from a pre-1.0.0 (beta) deployment? Read the 1.0.0 release notes first.** There is **no special upgrade path for 1.0.2 itself** β€” if you're already on 1.0.0 or 1.0.1 you upgrade in place with no migration. But 1.0.0 was the single-stack consolidation, and upgrading **from any beta** to 1.0.0 (and therefore to 1.0.2) is a **destructive backup β†’ teardown β†’ redeploy β†’ restore migration**, not an in-place `cdk deploy`. If you haven't already worked through it, do that before deploying 1.0.2: see [**Upgrading an existing deployment** (1.0.0 notes)](#upgrading-an-existing-deployment) below, or the published guide at . **Brand-new deployments need none of this.** + +--- + +## Highlights + +v1.0.2 is a small, security-focused patch on the 1.0.0 single-stack architecture with one notable behavior change. The headline is that **assistants can use tools again**: 1.0.0 had locked assistant chats to a knowledge-base-only, tool-free mode (#382), and this release reverts that so an assistant can once more leverage the user's selected MCP and built-in tools. Alongside it, this release lands a **CodeQL security-hardening sweep** (two HIGH findings around URL/host validation, a log-injection pass across 24 call sites, and a hardened CI checkout), remediates **6 Dependabot alerts** (Astro XSS/SSRF, esbuild dev-server file read, pydantic-settings path traversal), and fixes the **nightly coverage pipeline** that broke when the single-stack refactor moved its scripts. There is **no migration** β€” operators on 1.0.0 or 1.0.1 upgrade in place. + +--- + +## Assistants can use tools again + +1.0.0 introduced a deliberate restriction: assistant ("RAG") chats ran knowledge-base-grounded with **zero external tools** β€” the inference API forced `enabled_tools=[]` on assistant turns and the system prompt told the model it had no external tools (#382). That made assistants safe and predictable but also meant they couldn't search the web, hit an MCP server, or run code even when the user had those tools enabled. v1.0.2 reverts the restriction so assistants behave like a normal chat with the assistant's knowledge and instructions layered on top. + +What stays the same: knowledge-base context is still pre-stuffed into the user message, and the assistant's custom instructions still apply. What changes: the user's tool-picker selection now flows through to the agent on assistant turns, and assistant chats once again emit tool-use and MCP-App events. + +### Backend + +- `inference_api/chat/routes.py` β€” dropped the `enabled_tools=[]` override in the `rag_assistant_id` branch so the client's tool selection reaches the agent, and removed the "Knowledge Base Grounding / no external tools" directive from both the with-instructions and no-instructions system-prompt paths (restoring the pre-#382 prompt composition). + +### Frontend + +- `chat-request.service.ts` β€” no longer force-sends `enabled_tools=[]` on assistant turns; the user's tool-picker selection rides along (skills mode stays gated on non-assistant turns). +- `preview-chat.service.ts` β€” the editor preview now forwards the owner's enabled tools instead of `[]`, and builds the streaming assistant message as ordered content blocks (text interleaved with tool use) wired to `onToolUse`/`onToolResult`, so the shared message-list renders tool cards in the preview exactly like a consumer chat. + +### Test coverage + +Specs updated to assert tools are forwarded on both assistant and preview turns (`chat-request.service.spec.ts`, `preview-chat.service.spec.ts`). + +## πŸ› Bug fixes + +- **Nightly coverage pipeline was failing.** The single-stack refactor removed `scripts/stack-app-api/` and `scripts/stack-frontend/`, but `nightly.yml`'s `test-backend`, `test-frontend`, and `install-frontend` jobs still called them, so they died with "No such file or directory." The three scripts were ported to the sanctioned post-refactor layout β€” `scripts/backend/test.sh`, `scripts/frontend/install.sh`, and `scripts/frontend/test.sh` β€” with behavior preserved 1:1 (uv install/sync + `pytest --cov`; `npm ci` for frontend and infra; `ng test --no-watch --coverage`). (#518) + +## πŸ”’ Security + +This release closes a CodeQL sweep (#521) and 6 Dependabot alerts (#520), each with regression tests where applicable. + +**CodeQL code findings (#521):** + +- **HIGH `py/incomplete-url-substring-sanitization`** β€” `external_mcp_client` previously substring-checked the whole URL for an AWS marker before deciding to SigV4-sign. A crafted URL with the marker in a path, query, or userinfo segment could trick it into attaching IAM credentials to a non-AWS host. It now parses the host with `urlparse` and matches an **anchored suffix**. Covered by `TestAwsUrlHostSanitization` (adversarial URLs). +- **HIGH `js/regex/missing-regexp-anchor`** β€” `admin-tool.model` now parses the host via `new URL` and **anchors** the AWS-endpoint regexes (`$`) so a spoofed host can't satisfy the match. Covered by `admin-tool.model.spec.ts` (13 cases including spoofed hosts). +- **MEDIUM `py/log-injection`** (24 sites across 16 files) β€” a new `apis.shared.security.scrub_log()` helper neutralizes CR/LF and control characters; every flagged user-controlled log value is now wrapped. Covered by `test_log_sanitize.py`. +- **MEDIUM `actions/untrusted-checkout`** β€” all `inputs.ref` checkouts in `nightly-deploy-pipeline.yml` now set `persist-credentials: false`. +- **WARNING `py/regex/duplicate-in-character-class`** β€” removed a stray `[` from a `re.VERBOSE` comment that the regex parser misread as a character class. + +**Dependency CVE remediation (#520):** + +| Component | Package | From | To | Fix | +|---|---|---|---|---| +| docs-site | `astro` | 6.3.1 | 6.4.8 | Reflected XSS via slot name, host-header SSRF in prerendered error page, spread-attribute XSS | +| docs-site | `esbuild` | β€” | 0.28.1 (override) | Dev-server arbitrary file read (GHSA-g7r4-m6w7-qqqr) | +| frontend | `esbuild` | β€” | 0.28.1 (override) | Dev-server arbitrary file read (transitive via `@angular/build` 21.2.16) | +| backend | `pydantic-settings` | 2.13.1 | 2.14.2 | `NestedSecretsSettingsSource` symlink traversal / local file read (GHSA-4xgf-cpjx-pc3j) | + +## πŸš€ Deployment notes + +v1.0.2 is a patch on the single-stack `PlatformStack` architecture. Operators on 1.0.0 or 1.0.1 upgrade in place β€” **no migration, no new infrastructure, no new env vars.** + +- **Behavior change to be aware of:** after deploying, assistant chats will once again use whatever tools the user has enabled (web search, MCP servers, code interpreter, etc.) rather than running knowledge-base-only. If your deployment relied on assistants being tool-free, note that this 1.0.0 restriction has been intentionally reverted. +- The security and dependency fixes require no operator action beyond deploying the new images/SPA build. + +--- + # Release Notes β€” v1.0.1 **Release Date:** June 26, 2026 diff --git a/VERSION b/VERSION index 7dea76ed..6d7de6e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.1 +1.0.2 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e42acd3c..98ed0e0c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-stack" -version = "1.0.1" +version = "1.0.2" requires-python = ">=3.10" description = "Multi-agent conversational AI system with AWS Bedrock AgentCore" readme = "README.md" diff --git a/backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.py b/backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.py index 398bb3e2..bb63b9fa 100644 --- a/backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.py +++ b/backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.py @@ -119,6 +119,7 @@ def _parse_sheet_inventory(bootstrap_stdout: str) -> Dict[str, Any]: if isinstance(names, list): result["skipped_preview"] = [str(n) for n in names] except (ValueError, SyntaxError): + # Best-effort parse β€” ignore malformed preview metadata. pass elif stripped.startswith("sheet|"): parts = stripped.split("|") diff --git a/backend/src/agents/builtin_tools/spreadsheet_analysis/list_spreadsheets_tool.py b/backend/src/agents/builtin_tools/spreadsheet_analysis/list_spreadsheets_tool.py index 45744f38..c63923dd 100644 --- a/backend/src/agents/builtin_tools/spreadsheet_analysis/list_spreadsheets_tool.py +++ b/backend/src/agents/builtin_tools/spreadsheet_analysis/list_spreadsheets_tool.py @@ -14,6 +14,8 @@ from apis.shared.files.models import is_tabular_file +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -121,7 +123,7 @@ def _query() -> Dict[str, Any]: return files except Exception as e: - logger.error(f"Error querying KB files for assistant {assistant_id}: {e}") + logger.error(f"Error querying KB files for assistant {scrub_log(assistant_id)}: {scrub_log(e)}") return [] @@ -154,5 +156,5 @@ async def _get_session_files(session_id: str) -> List[Dict[str, Any]]: return files except Exception as e: - logger.error(f"Error querying session files for {session_id}: {e}") + logger.error(f"Error querying session files for {scrub_log(session_id)}: {scrub_log(e)}") return [] diff --git a/backend/src/agents/main_agent/agent_types.py b/backend/src/agents/main_agent/agent_types.py index 92bf033c..377fdd72 100644 --- a/backend/src/agents/main_agent/agent_types.py +++ b/backend/src/agents/main_agent/agent_types.py @@ -10,6 +10,8 @@ from agents.main_agent.base_agent import BaseAgent +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -47,7 +49,7 @@ def create_agent(agent_type: str = "chat", **kwargs) -> BaseAgent: available = ", ".join(sorted(_AGENT_TYPES.keys())) raise ValueError(f"Unknown agent_type '{agent_type}'. Available: {available}") - logger.info(f"Creating {agent_type} agent ({agent_class.__name__})") + logger.info(f"Creating {scrub_log(agent_type)} agent ({agent_class.__name__})") return agent_class(**kwargs) diff --git a/backend/src/agents/main_agent/base_agent.py b/backend/src/agents/main_agent/base_agent.py index 886ac130..caf4240f 100644 --- a/backend/src/agents/main_agent/base_agent.py +++ b/backend/src/agents/main_agent/base_agent.py @@ -178,7 +178,6 @@ def __init__( @abstractmethod def _create_agent(self) -> None: """Create the specific agent type. Subclasses must implement.""" - ... @abstractmethod async def stream_async( @@ -204,7 +203,6 @@ async def stream_async( assistant message already in restored history (assistant-prefill), rather than answering a fresh instruction. """ - ... def _register_external_mcp_tools(self) -> None: """ diff --git a/backend/src/agents/main_agent/integrations/external_mcp_client.py b/backend/src/agents/main_agent/integrations/external_mcp_client.py index 54ec8483..5b363d96 100644 --- a/backend/src/agents/main_agent/integrations/external_mcp_client.py +++ b/backend/src/agents/main_agent/integrations/external_mcp_client.py @@ -16,6 +16,7 @@ import logging import re from typing import Any, Callable, Optional, List, Set +from urllib.parse import urlparse from mcp.client.streamable_http import streamablehttp_client from strands.tools.mcp import MCPClient @@ -38,6 +39,22 @@ logger = logging.getLogger(__name__) +def _aws_hostname(url: str) -> str: + """Return the lowercased hostname of ``url``, or ``""`` if it can't be parsed. + + AWS-endpoint detection must match against the *host* only. Substring-matching + the whole URL (e.g. ``".amazonaws.com" in url``) is unsafe: an attacker can + place the marker in a path or query component + (``https://evil.example/?x=.execute-api.us-east-1.amazonaws.com``) and trick + the caller into SigV4-signing a request to a non-AWS host, leaking the task's + IAM credentials. Parsing the host and anchoring the suffix prevents that. + """ + try: + return (urlparse(url).hostname or "").lower() + except ValueError: + return "" + + def extract_region_from_url(url: str) -> Optional[str]: """ Extract AWS region from Lambda Function URL or API Gateway URL. @@ -52,14 +69,15 @@ def extract_region_from_url(url: str) -> Optional[str]: Returns: AWS region or None if not extractable """ + host = _aws_hostname(url) patterns = [ - r"\.lambda-url\.([a-z0-9-]+)\.on\.aws", - r"\.execute-api\.([a-z0-9-]+)\.amazonaws\.com", - r"\.bedrock-agentcore\.([a-z0-9-]+)\.amazonaws\.com", + r"\.lambda-url\.([a-z0-9-]+)\.on\.aws$", + r"\.execute-api\.([a-z0-9-]+)\.amazonaws\.com$", + r"\.bedrock-agentcore\.([a-z0-9-]+)\.amazonaws\.com$", ] for pattern in patterns: - match = re.search(pattern, url) + match = re.search(pattern, host) if match: return match.group(1) @@ -88,11 +106,12 @@ def detect_aws_service_from_url(url: str) -> Optional[str]: AWS service name for SigV4 signing, or None if the URL doesn't match a known AWS service. """ - if ".lambda-url." in url and ".on.aws" in url: + host = _aws_hostname(url) + if host.endswith(".on.aws") and ".lambda-url." in host: return "lambda" - elif ".execute-api." in url and ".amazonaws.com" in url: + elif host.endswith(".amazonaws.com") and ".execute-api." in host: return "execute-api" - elif ".bedrock-agentcore." in url and ".amazonaws.com" in url: + elif host.endswith(".amazonaws.com") and ".bedrock-agentcore." in host: return "bedrock-agentcore" else: logger.debug( diff --git a/backend/src/agents/main_agent/session/persistence.py b/backend/src/agents/main_agent/session/persistence.py index ec9840b8..60170dd1 100644 --- a/backend/src/agents/main_agent/session/persistence.py +++ b/backend/src/agents/main_agent/session/persistence.py @@ -29,6 +29,8 @@ from strands.types.content import Message from strands.types.session import SessionMessage +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -77,7 +79,7 @@ def persist_synthetic_messages( ) if target_manager is None: logger.error( - f"Cannot persist messages to session {session_id}: " + f"Cannot persist messages to session {scrub_log(session_id)}: " f"session manager {type(session_manager).__name__} has no create_message method" ) return False @@ -88,6 +90,6 @@ def persist_synthetic_messages( target_manager.create_message(session_id, agent_id, session_msg) logger.info( - f"πŸ’Ύ Persisted {len(messages)} synthetic message(s) to session {session_id}" + f"πŸ’Ύ Persisted {len(messages)} synthetic message(s) to session {scrub_log(session_id)}" ) return True diff --git a/backend/src/agents/main_agent/skills/skill_tools.py b/backend/src/agents/main_agent/skills/skill_tools.py index 3ce0750a..fa8cf6ab 100644 --- a/backend/src/agents/main_agent/skills/skill_tools.py +++ b/backend/src/agents/main_agent/skills/skill_tools.py @@ -25,7 +25,7 @@ import asyncio import json import logging -from typing import Any, Optional +from typing import Any from strands import tool @@ -137,6 +137,7 @@ def _execute(registry: Any, skill_name: str, tool_name: str, tool_input: Any = N try: tool_input = json.loads(tool_input) except (json.JSONDecodeError, TypeError): + # Not JSON β€” leave tool_input as its original value. pass if tool_input is None: diff --git a/backend/src/apis/app_api/admin/routes.py b/backend/src/apis/app_api/admin/routes.py index 01f5d761..0b451f3a 100644 --- a/backend/src/apis/app_api/admin/routes.py +++ b/backend/src/apis/app_api/admin/routes.py @@ -10,7 +10,7 @@ import os import re import boto3 -from botocore.exceptions import ClientError, BotoCoreError +from botocore.exceptions import BotoCoreError from .models import ( BedrockModelsResponse, @@ -30,8 +30,6 @@ ) from apis.shared.auth import User, require_admin from apis.shared.feature_flags import skills_enabled -from apis.shared.sessions.metadata import list_user_sessions, get_session_metadata -from apis.shared.sessions.messages import get_messages from apis.shared.models.managed_models import ( create_managed_model, get_managed_model, diff --git a/backend/src/apis/app_api/artifacts/service.py b/backend/src/apis/app_api/artifacts/service.py index 6a8ac003..a740d72e 100644 --- a/backend/src/apis/app_api/artifacts/service.py +++ b/backend/src/apis/app_api/artifacts/service.py @@ -24,6 +24,8 @@ from boto3.dynamodb.conditions import Key from botocore.exceptions import ClientError +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) # Frozen contract β€” must match the render Lambda's _verify_token. @@ -223,8 +225,8 @@ def mint( logger.info( "minted render token user=%s artifact=%s v=%s", user_id, - artifact_id, - version, + scrub_log(artifact_id), + scrub_log(version), ) return f"{origin}/?t={token}", exp diff --git a/backend/src/apis/app_api/assistants/routes.py b/backend/src/apis/app_api/assistants/routes.py index 2ac75456..aa810eea 100644 --- a/backend/src/apis/app_api/assistants/routes.py +++ b/backend/src/apis/app_api/assistants/routes.py @@ -36,7 +36,6 @@ create_assistant, create_assistant_draft, delete_assistant, - get_assistant, get_assistant_with_access_check, list_assistant_shares, list_shared_with_user, diff --git a/backend/src/apis/app_api/connectors/routes.py b/backend/src/apis/app_api/connectors/routes.py index 43b1ee59..0e5b82b8 100644 --- a/backend/src/apis/app_api/connectors/routes.py +++ b/backend/src/apis/app_api/connectors/routes.py @@ -50,6 +50,8 @@ ) from apis.shared.rbac.service import AppRoleService, get_app_role_service +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -382,8 +384,8 @@ async def complete_consent( logger.error( "CompleteResourceTokenAuth failed for user=%s provider=%s: %s", current_user.user_id, - body.provider_id, - err, + scrub_log(body.provider_id), + scrub_log(err), ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, @@ -401,7 +403,7 @@ async def complete_consent( logger.info( "Completed OAuth consent for user=%s provider=%s", current_user.user_id, - body.provider_id, + scrub_log(body.provider_id), ) return CompleteConsentResponse(ok=True) diff --git a/backend/src/apis/app_api/documents/ingestion/processors/xlsx_chunker.py b/backend/src/apis/app_api/documents/ingestion/processors/xlsx_chunker.py index 70ea6827..3f896050 100644 --- a/backend/src/apis/app_api/documents/ingestion/processors/xlsx_chunker.py +++ b/backend/src/apis/app_api/documents/ingestion/processors/xlsx_chunker.py @@ -48,6 +48,7 @@ def _is_likely_header(row: tuple, next_row: tuple = None) -> bool: float(stripped.replace(",", "")) continue # It's a number in string form except ValueError: + # Not a numeric string β€” fall through and count it as text. pass text_count += 1 # Non-string types (int, float, datetime) are not header-like @@ -67,6 +68,7 @@ def _is_likely_header(row: tuple, next_row: tuple = None) -> bool: float(cell.strip().replace(",", "")) numeric_count += 1 except ValueError: + # Not numeric β€” leave numeric_count unchanged. pass # Data row should have at least some numbers if next_non_empty and numeric_count > 0: diff --git a/backend/src/apis/app_api/documents/services/import_service.py b/backend/src/apis/app_api/documents/services/import_service.py index f583c98c..2dd3796d 100644 --- a/backend/src/apis/app_api/documents/services/import_service.py +++ b/backend/src/apis/app_api/documents/services/import_service.py @@ -15,7 +15,6 @@ import asyncio import logging -import os from typing import List, Tuple import boto3 diff --git a/backend/src/apis/app_api/export_targets/routes.py b/backend/src/apis/app_api/export_targets/routes.py index dd139b94..0b289a19 100644 --- a/backend/src/apis/app_api/export_targets/routes.py +++ b/backend/src/apis/app_api/export_targets/routes.py @@ -61,6 +61,8 @@ resolve_export_target_token, ) +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) router = APIRouter(tags=["export-targets"]) @@ -207,7 +209,7 @@ async def _collect_transcript(session_id: str, user_id: str) -> List[MessageResp return messages logger.warning( "Export for session %s hit the %d-page cap; transcript may be truncated", - session_id, + scrub_log(session_id), _MAX_EXPORT_PAGES, ) return messages @@ -348,7 +350,7 @@ async def export_session( ) except ExportTargetError as err: logger.warning( - "create_document failed for connector %s: %s", request.connector_id, err + "create_document failed for connector %s: %s", scrub_log(request.connector_id), scrub_log(err) ) raise http_error_for_export_target_error(err) diff --git a/backend/src/apis/app_api/file_sources/routes.py b/backend/src/apis/app_api/file_sources/routes.py index c8c7cf1b..93efd033 100644 --- a/backend/src/apis/app_api/file_sources/routes.py +++ b/backend/src/apis/app_api/file_sources/routes.py @@ -45,6 +45,8 @@ resolve_file_source_token, ) +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) router = APIRouter(tags=["file-sources"]) @@ -174,7 +176,7 @@ async def list_file_source_roots( try: roots = await adapter.list_roots(access_token) except FileSourceError as err: - logger.warning("list_roots failed for connector %s: %s", provider_id, err) + logger.warning("list_roots failed for connector %s: %s", scrub_log(provider_id), scrub_log(err)) raise http_error_for_file_source_error(err) return SourceRootsResponse(roots=roots) @@ -201,7 +203,7 @@ async def browse_file_source( try: return await adapter.browse(access_token, folder_id, cursor) except FileSourceError as err: - logger.warning("browse failed for connector %s: %s", provider_id, err) + logger.warning("browse failed for connector %s: %s", scrub_log(provider_id), scrub_log(err)) raise http_error_for_file_source_error(err) @@ -225,5 +227,5 @@ async def search_file_source( try: return await adapter.search(access_token, query, cursor) except FileSourceError as err: - logger.warning("search failed for connector %s: %s", provider_id, err) + logger.warning("search failed for connector %s: %s", scrub_log(provider_id), scrub_log(err)) raise http_error_for_file_source_error(err) diff --git a/backend/src/apis/app_api/files/routes.py b/backend/src/apis/app_api/files/routes.py index d3a20f81..4ec91200 100644 --- a/backend/src/apis/app_api/files/routes.py +++ b/backend/src/apis/app_api/files/routes.py @@ -36,6 +36,8 @@ ) from .thumbnails import ThumbnailRenderError, ThumbnailUnsupportedError +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) router = APIRouter(prefix="/files", tags=["files"]) @@ -219,7 +221,7 @@ async def get_thumbnail( detail=str(e), ) except ThumbnailRenderError as e: - logger.warning(f"Thumbnail render failed for {upload_id}: {e}") + logger.warning(f"Thumbnail render failed for {scrub_log(upload_id)}: {scrub_log(e)}") raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Could not render a thumbnail for this file", diff --git a/backend/src/apis/app_api/files/service.py b/backend/src/apis/app_api/files/service.py index b837d26c..08f778c8 100644 --- a/backend/src/apis/app_api/files/service.py +++ b/backend/src/apis/app_api/files/service.py @@ -15,6 +15,8 @@ from botocore.config import Config from botocore.exceptions import ClientError +from apis.shared.security.log_sanitize import scrub_log + from apis.shared.files.models import ( FileMetadata, FileStatus, @@ -437,7 +439,7 @@ async def get_text_snippet( ) body = response["Body"].read() except ClientError as e: - logger.warning(f"Failed to read snippet for {upload_id}: {e}") + logger.warning(f"Failed to read snippet for {scrub_log(upload_id)}: {scrub_log(e)}") return TextSnippetResponse( upload_id=upload_id, snippet="", diff --git a/backend/src/apis/app_api/sessions/routes.py b/backend/src/apis/app_api/sessions/routes.py index d682a9dd..a93e020d 100644 --- a/backend/src/apis/app_api/sessions/routes.py +++ b/backend/src/apis/app_api/sessions/routes.py @@ -32,6 +32,8 @@ from apis.shared.auth.models import User from apis.shared.system_prompts.service import get_system_prompts_service +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) router = APIRouter(prefix="/sessions", tags=["sessions"]) @@ -218,7 +220,7 @@ async def update_session_metadata_endpoint( ): logger.warning( "PUT /sessions/%s/metadata: session id is taken under a different user; refusing", - session_id, + scrub_log(session_id), ) raise HTTPException( status_code=404, diff --git a/backend/src/apis/app_api/web_sources/crawler.py b/backend/src/apis/app_api/web_sources/crawler.py index 2291073a..cfbaeb04 100644 --- a/backend/src/apis/app_api/web_sources/crawler.py +++ b/backend/src/apis/app_api/web_sources/crawler.py @@ -24,7 +24,6 @@ import time from collections import defaultdict from typing import Awaitable, Callable, Dict, List, Optional, Set, Tuple -from urllib.parse import urljoin from urllib.robotparser import RobotFileParser import httpx diff --git a/backend/src/apis/app_api/web_sources/routes.py b/backend/src/apis/app_api/web_sources/routes.py index 019195c9..4ad9a2a9 100644 --- a/backend/src/apis/app_api/web_sources/routes.py +++ b/backend/src/apis/app_api/web_sources/routes.py @@ -50,6 +50,8 @@ from apis.shared.assistants.service import get_assistant from apis.shared.auth import User, get_current_user_from_session +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) router = APIRouter( @@ -136,10 +138,10 @@ async def start_crawl( task.add_done_callback(_BACKGROUND_CRAWLS.discard) logger.info( "Kicked off crawl %s for assistant %s (root_document=%s url=%s)", - job.crawl_id, - assistant_id, - document_id, - normalized, + scrub_log(job.crawl_id), + scrub_log(assistant_id), + scrub_log(document_id), + scrub_log(normalized), ) return StartCrawlResponse( diff --git a/backend/src/apis/inference_api/chat/app_context_dispatch.py b/backend/src/apis/inference_api/chat/app_context_dispatch.py index 031513cd..39e1739f 100644 --- a/backend/src/apis/inference_api/chat/app_context_dispatch.py +++ b/backend/src/apis/inference_api/chat/app_context_dispatch.py @@ -39,6 +39,8 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Optional +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) # Top-level Strands `agent.state` key that holds all MCP Apps host state. @@ -120,7 +122,7 @@ def dispatch_app_context_update( logger.info( "mcp-apps: stored model context (resource=%s, pending=%d)", - resource_uri, + scrub_log(resource_uri), len(ctx), ) return {"resourceUri": resource_uri, "status": "stored", "pending": len(ctx)} diff --git a/backend/src/apis/inference_api/chat/app_tool_dispatch.py b/backend/src/apis/inference_api/chat/app_tool_dispatch.py index 4c8a33ed..3530f019 100644 --- a/backend/src/apis/inference_api/chat/app_tool_dispatch.py +++ b/backend/src/apis/inference_api/chat/app_tool_dispatch.py @@ -30,6 +30,8 @@ from apis.shared.mcp_apps.broker import get_app_tool_event_broker +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -141,9 +143,9 @@ async def dispatch_app_tool_call( except Exception as exc: # noqa: BLE001 - surfaced to the App as an error logger.warning( "app tools/call dispatch failed (tool=%s session=%s): %s", - tool_name, - session_id, - exc, + scrub_log(tool_name), + scrub_log(session_id), + scrub_log(exc), ) raise AppToolCallError( f"Tool '{tool_name}' failed to execute", code=502 diff --git a/backend/src/apis/inference_api/chat/routes.py b/backend/src/apis/inference_api/chat/routes.py index 805e74cd..3df58c02 100644 --- a/backend/src/apis/inference_api/chat/routes.py +++ b/backend/src/apis/inference_api/chat/routes.py @@ -58,6 +58,8 @@ should_resolve_custom_prompt, ) +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) # Router with no prefix - endpoints will be at root level @@ -359,7 +361,7 @@ def _build_spreadsheet_tools( if "analyze_spreadsheet" in requested: tools.append(make_analyze_tool(assistant_id, session_id, user_id)) - logger.info(f"Created {len(tools)} spreadsheet analysis tools (assistant={assistant_id})") + logger.info(f"Created {len(tools)} spreadsheet analysis tools (assistant={scrub_log(assistant_id)})") return tools @@ -1105,14 +1107,6 @@ async def invocations(request: InvocationRequest, current_user: User = Depends(g logger.info("Assistant RAG requested") logger.info("Processing for authenticated user") - # Assistants are KB-grounded with no external tools available to the consumer. - # Override any tools the client sent β€” server is the source of truth here. - if input_data.enabled_tools: - logger.warning( - "Ignoring enabled_tools on assistant chat (assistants are KB-only)" - ) - input_data.enabled_tools = [] - # 1. Check if session already has an assistant attached # If it does, verify it's the same assistant (can't change assistants mid-session) # If it doesn't, verify session has no messages (can only attach to new sessions) @@ -1223,13 +1217,6 @@ async def invocations(request: InvocationRequest, current_user: User = Depends(g preview_instructions_override = input_data.system_prompt if is_preview_session(input_data.session_id) and input_data.system_prompt else None effective_instructions = preview_instructions_override or assistant.instructions - kb_grounding_directive = ( - "## Knowledge Base Grounding\n\n" - "Answer using only the knowledge base context provided with the user's message. " - "If the context does not cover the question, say so plainly rather than guessing. " - "You have no external tools available." - ) - if effective_instructions: # Import here to avoid circular dependency from agents.main_agent.core.system_prompt_builder import SystemPromptBuilder @@ -1239,7 +1226,7 @@ async def invocations(request: InvocationRequest, current_user: User = Depends(g base_prompt = base_prompt_builder.build(include_date=True) # Append assistant instructions to the base prompt - system_prompt = f"{base_prompt}\n\n{kb_grounding_directive}\n\n## Assistant-Specific Instructions\n\n{effective_instructions}" + system_prompt = f"{base_prompt}\n\n## Assistant-Specific Instructions\n\n{effective_instructions}" if preview_instructions_override: logger.info( "Using live preview instructions override" @@ -1252,13 +1239,13 @@ async def invocations(request: InvocationRequest, current_user: User = Depends(g else: # No assistant instructions - use base prompt if no system_prompt provided logger.warning("No instructions found on assistant!") - from agents.main_agent.core.system_prompt_builder import SystemPromptBuilder + if not system_prompt: + from agents.main_agent.core.system_prompt_builder import SystemPromptBuilder - base_prompt_builder = SystemPromptBuilder() - base_prompt = base_prompt_builder.build(include_date=True) - system_prompt = f"{base_prompt}\n\n{kb_grounding_directive}" + base_prompt_builder = SystemPromptBuilder() + system_prompt = base_prompt_builder.build(include_date=True) logger.info( - "Assistant has no instructions - using fallback system prompt with KB grounding" + "Assistant has no instructions - using fallback system prompt" ) # 6. Save assistant_id to session preferences (persist for future loads) @@ -1372,7 +1359,6 @@ async def invocations(request: InvocationRequest, current_user: User = Depends(g detail="Paused turn expired; restart the turn.", ) - caching_enabled = snapshot.caching_enabled # Snapshot wins on resume so an authorized turn finishes against the # exact param shape it was authorized for, even if admin defaults # have since changed. Fall back to the legacy fields for snapshots diff --git a/backend/src/apis/inference_api/chat/service.py b/backend/src/apis/inference_api/chat/service.py index 464481a9..14deb742 100644 --- a/backend/src/apis/inference_api/chat/service.py +++ b/backend/src/apis/inference_api/chat/service.py @@ -16,6 +16,8 @@ from agents.main_agent.base_agent import BaseAgent from apis.shared.sessions.metadata import update_session_title +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -210,7 +212,7 @@ async def get_agent( logger.warning( "Cached agent is paused on an interrupt but request is not a resume; " "evicting and rebuilding (session=%s user=%s)", - session_id, user_id, + scrub_log(session_id), scrub_log(user_id), ) del _agent_cache[cache_key] else: diff --git a/backend/src/apis/inference_api/chat/system_prompt_resolver.py b/backend/src/apis/inference_api/chat/system_prompt_resolver.py index 9619e5ef..c3691e98 100644 --- a/backend/src/apis/inference_api/chat/system_prompt_resolver.py +++ b/backend/src/apis/inference_api/chat/system_prompt_resolver.py @@ -36,6 +36,8 @@ ) from apis.shared.system_prompts.service import get_system_prompts_service +from apis.shared.security.log_sanitize import scrub_log + logger = logging.getLogger(__name__) @@ -115,7 +117,7 @@ async def resolve_active_prompt_text( custom_prompt = await get_system_prompts_service().get_enabled_prompt(active_prompt_id) if not custom_prompt: logger.info( - f"Custom prompt {active_prompt_id!r} not found or disabled β€” skipping" + f"Custom prompt {scrub_log(active_prompt_id)!r} not found or disabled β€” skipping" ) return None diff --git a/backend/src/apis/shared/mcp_apps/partial_json.py b/backend/src/apis/shared/mcp_apps/partial_json.py index 451c863b..bce04258 100644 --- a/backend/src/apis/shared/mcp_apps/partial_json.py +++ b/backend/src/apis/shared/mcp_apps/partial_json.py @@ -130,6 +130,7 @@ def heal_partial_json(raw: Optional[str]) -> Optional[Dict[str, Any]]: if isinstance(parsed, dict): return parsed except (ValueError, TypeError): + # Candidate isn't valid JSON yet β€” keep shrinking the window. pass end -= 1 return None diff --git a/backend/src/apis/shared/security/__init__.py b/backend/src/apis/shared/security/__init__.py index e2d17c5a..0e42ae54 100644 --- a/backend/src/apis/shared/security/__init__.py +++ b/backend/src/apis/shared/security/__init__.py @@ -25,6 +25,7 @@ PolicyError, validate_diagram_code, ) +from apis.shared.security.log_sanitize import scrub_log __all__ = [ "UrlValidationError", @@ -39,4 +40,5 @@ "register_validation_error_handler", "PolicyError", "validate_diagram_code", + "scrub_log", ] diff --git a/backend/src/apis/shared/security/log_sanitize.py b/backend/src/apis/shared/security/log_sanitize.py new file mode 100644 index 00000000..0fa06328 --- /dev/null +++ b/backend/src/apis/shared/security/log_sanitize.py @@ -0,0 +1,36 @@ +"""Log-injection sanitization. + +User-controlled values written into log records must have their line +terminators neutralized; otherwise an attacker can embed ``\\r``/``\\n`` in an +input (a filename, connector id, tool name, error message, …) and forge or +inject additional log lines, corrupting audit trails and log-based alerting. + +This addresses CodeQL ``py/log-injection``. Wrap any user-influenced value +that flows into a ``logger.*`` call with :func:`scrub_log`. + + logger.warning("list_roots failed for connector %s: %s", + scrub_log(provider_id), scrub_log(err)) +""" + +from typing import Any + +# C0/C1 control characters except ordinary tab (handled explicitly below). +_CONTROL_TRANSLATION = { + i: None + for i in range(0x20) + if i not in (0x09,) # keep \t out of the "delete" set; escaped below +} +_CONTROL_TRANSLATION.update({0x7F: None}) + + +def scrub_log(value: Any) -> str: + """Return ``str(value)`` with line breaks and control characters neutralized + so it is safe to embed in a single log record. + + - ``\\r`` and ``\\n`` (and ``\\t``) are replaced with visible escapes so the + original content is preserved for debugging but cannot start a new line. + - Other C0/C1 control characters are stripped. + """ + text = str(value) + text = text.replace("\r", "\\r").replace("\n", "\\n").replace("\t", "\\t") + return text.translate(_CONTROL_TRANSLATION) diff --git a/backend/src/apis/shared/security/url_validator.py b/backend/src/apis/shared/security/url_validator.py index fc87357e..16419cf6 100644 --- a/backend/src/apis/shared/security/url_validator.py +++ b/backend/src/apis/shared/security/url_validator.py @@ -74,6 +74,7 @@ def _is_forbidden_address(addr: ipaddress.IPv4Address | ipaddress.IPv6Address) - if addr in cgnat: return True except ValueError: + # Not an IP literal β€” fall through to the metadata-hostname check. pass if str(addr) in _METADATA_ADDRESSES: return True diff --git a/backend/src/apis/shared/sessions_bff/lock.py b/backend/src/apis/shared/sessions_bff/lock.py index b85c8ebe..2efd402a 100644 --- a/backend/src/apis/shared/sessions_bff/lock.py +++ b/backend/src/apis/shared/sessions_bff/lock.py @@ -18,7 +18,6 @@ import asyncio from threading import Lock as _ThreadLock -from typing import Dict from weakref import WeakValueDictionary # We use a WeakValueDictionary so locks don't leak indefinitely as session diff --git a/backend/src/apis/shared/user_menu_links/models.py b/backend/src/apis/shared/user_menu_links/models.py index 81577613..5394ccb5 100644 --- a/backend/src/apis/shared/user_menu_links/models.py +++ b/backend/src/apis/shared/user_menu_links/models.py @@ -9,7 +9,7 @@ PK becomes ``USER_MENU_LINKS#`` without touching the SK shape. """ -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime, timezone from typing import Any, Dict, List, Literal, Optional diff --git a/backend/tests/agents/main_agent/integrations/test_external_mcp_client.py b/backend/tests/agents/main_agent/integrations/test_external_mcp_client.py index 8b2fe671..19db6b5f 100644 --- a/backend/tests/agents/main_agent/integrations/test_external_mcp_client.py +++ b/backend/tests/agents/main_agent/integrations/test_external_mcp_client.py @@ -77,6 +77,63 @@ def test_returns_none_for_unknown_url(self): assert detect_aws_service_from_url(url) is None +class TestAwsUrlHostSanitization: + """Regression tests for CodeQL py/incomplete-url-substring-sanitization + (alert #695): AWS-endpoint markers must only be honored when they appear + in the URL *host*, not anywhere in the string. Otherwise an attacker can + smuggle the marker into a path/query and trick the caller into attaching + SigV4 IAM credentials to a request bound for a non-AWS host.""" + + # (description, malicious_url) β€” every one of these must NOT be treated as AWS. + SPOOFED_URLS = [ + ( + "marker in query string", + "https://evil.example/?x=.execute-api.us-east-1.amazonaws.com", + ), + ( + "marker in path", + "https://evil.example/.lambda-url.us-east-1.on.aws/mcp", + ), + ( + "real AWS suffix as a non-terminal label of an attacker domain", + "https://x.execute-api.us-east-1.amazonaws.com.evil.example/mcp", + ), + ( + "marker in userinfo", + "https://.execute-api.us-east-1.amazonaws.com@evil.example/mcp", + ), + ( + "agentcore marker in fragment", + "https://evil.example/#.bedrock-agentcore.us-east-1.amazonaws.com", + ), + ] + + @pytest.mark.parametrize("desc,url", SPOOFED_URLS) + def test_detect_service_rejects_spoofed_url(self, desc, url): + assert detect_aws_service_from_url(url) is None, desc + + @pytest.mark.parametrize("desc,url", SPOOFED_URLS) + def test_extract_region_rejects_spoofed_url(self, desc, url): + assert extract_region_from_url(url) is None, desc + + def test_unparseable_url_is_not_aws(self): + assert detect_aws_service_from_url("not a url") is None + assert extract_region_from_url("not a url") is None + + def test_legitimate_hosts_still_detected(self): + """The hardening must not regress genuine AWS endpoints (incl. paths/ports).""" + assert ( + detect_aws_service_from_url( + "https://x.execute-api.us-east-1.amazonaws.com:443/prod" + ) + == "execute-api" + ) + assert ( + extract_region_from_url("https://x.lambda-url.eu-west-1.on.aws/") + == "eu-west-1" + ) + + class TestProviderForClient: """The integration's MCPClient -> provider_id map is what `OAuthConsentHook.provider_lookup` consults.""" diff --git a/backend/tests/shared/test_log_sanitize.py b/backend/tests/shared/test_log_sanitize.py new file mode 100644 index 00000000..382c908d --- /dev/null +++ b/backend/tests/shared/test_log_sanitize.py @@ -0,0 +1,47 @@ +"""Tests for the log-injection sanitizer (CodeQL py/log-injection). + +`scrub_log` must neutralize line terminators and control characters so a +user-controlled value can't forge or inject extra log lines. +""" + +from apis.shared.security.log_sanitize import scrub_log +from apis.shared.security import scrub_log as scrub_log_reexport + + +class TestScrubLog: + def test_replaces_newlines_and_carriage_returns(self): + out = scrub_log("alice\nADMIN login succeeded") + assert "\n" not in out + assert "\r" not in out + assert out == "alice\\nADMIN login succeeded" + + def test_neutralizes_crlf_forged_log_line(self): + forged = "user1\r\n2024-01-01 ERROR forged entry" + out = scrub_log(forged) + assert "\r" not in out and "\n" not in out + # The whole payload collapses onto a single line. + assert "\n" not in out.splitlines()[0] + assert len(out.splitlines()) == 1 + + def test_replaces_tabs(self): + assert scrub_log("a\tb") == "a\\tb" + + def test_strips_other_control_characters(self): + # NUL, bell, escape (ANSI), and DEL must be removed entirely. + assert scrub_log("a\x00b\x07c\x1bd\x7fe") == "abcde" + + def test_leaves_ordinary_text_untouched(self): + assert scrub_log("provider-google_workspace.v2") == "provider-google_workspace.v2" + + def test_coerces_non_string_values(self): + assert scrub_log(42) == "42" + assert scrub_log(None) == "None" + + def test_handles_exception_objects(self): + err = ValueError("bad input\ninjected line") + out = scrub_log(err) + assert "\n" not in out + assert out == "bad input\\ninjected line" + + def test_reexported_from_security_package(self): + assert scrub_log_reexport is scrub_log diff --git a/backend/uv.lock b/backend/uv.lock index 7dd92f86..c6244a6d 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ [[package]] name = "agentcore-stack" -version = "1.0.1" +version = "1.0.2" source = { editable = "." } dependencies = [ { name = "aiofiles" }, @@ -3858,16 +3858,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index bfb5ff51..4fe69c95 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "@astrojs/starlight": "0.39.3", - "astro": "6.3.1", + "astro": "^6.4.6", "sharp": "0.34.5" } }, @@ -309,9 +309,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -357,9 +357,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -373,9 +373,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -389,9 +389,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -405,9 +405,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -421,9 +421,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -437,9 +437,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -453,9 +453,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -469,9 +469,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -485,9 +485,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -501,9 +501,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -517,9 +517,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -533,9 +533,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -549,9 +549,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -565,9 +565,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -581,9 +581,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -597,9 +597,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -613,9 +613,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -629,9 +629,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -645,9 +645,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -661,9 +661,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -677,9 +677,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -693,9 +693,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -709,9 +709,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -2019,14 +2019,14 @@ } }, "node_modules/astro": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.3.1.tgz", - "integrity": "sha512-atz6dmkE3Gu24bDgb7g2RE/BYnKqPYIHd6hTUM1UXvu/i7qNZOKLAqEHvgYpv9PQVcgWsXpk4/OOXZ0E/FzvSQ==", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.8.tgz", + "integrity": "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==", "license": "MIT", "dependencies": { "@astrojs/compiler": "^4.0.0", - "@astrojs/internal-helpers": "0.9.0", - "@astrojs/markdown-remark": "7.1.1", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", @@ -2038,7 +2038,7 @@ "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", - "devalue": "^5.6.3", + "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", @@ -2108,56 +2108,6 @@ "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, - "node_modules/astro/node_modules/@astrojs/internal-helpers": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.0.tgz", - "integrity": "sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==", - "license": "MIT", - "dependencies": { - "picomatch": "^4.0.4" - } - }, - "node_modules/astro/node_modules/@astrojs/markdown-remark": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.1.tgz", - "integrity": "sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==", - "license": "MIT", - "dependencies": { - "@astrojs/internal-helpers": "0.9.0", - "@astrojs/prism": "4.0.1", - "github-slugger": "^2.0.0", - "hast-util-from-html": "^2.0.3", - "hast-util-to-text": "^4.0.2", - "js-yaml": "^4.1.1", - "mdast-util-definitions": "^6.0.0", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "retext-smartypants": "^6.2.0", - "shiki": "^4.0.0", - "smol-toml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "unist-util-visit-parents": "^6.0.2", - "vfile": "^6.0.3" - } - }, - "node_modules/astro/node_modules/@astrojs/prism": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", - "integrity": "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==", - "license": "MIT", - "dependencies": { - "prismjs": "^1.30.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2693,9 +2643,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2705,32 +2655,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escape-string-regexp": { diff --git a/docs-site/package.json b/docs-site/package.json index 901a119e..1356da87 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -11,7 +11,10 @@ }, "dependencies": { "@astrojs/starlight": "0.39.3", - "astro": "6.3.1", + "astro": "^6.4.6", "sharp": "0.34.5" + }, + "overrides": { + "esbuild": "^0.28.1" } } \ No newline at end of file diff --git a/frontend/ai.client/package-lock.json b/frontend/ai.client/package-lock.json index 54a3dcee..9b3bcf4f 100644 --- a/frontend/ai.client/package-lock.json +++ b/frontend/ai.client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai.client", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai.client", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@angular/cdk": "21.2.14", "@angular/common": "21.2.17", @@ -1679,9 +1679,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -1696,9 +1696,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -1713,9 +1713,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -1730,9 +1730,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -1747,9 +1747,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -1764,9 +1764,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -1781,9 +1781,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -1798,9 +1798,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -1815,9 +1815,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -1832,9 +1832,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -1849,9 +1849,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -1866,9 +1866,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1883,9 +1883,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1900,9 +1900,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1917,9 +1917,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1934,9 +1934,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1951,9 +1951,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1968,9 +1968,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1985,9 +1985,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -2002,9 +2002,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -2019,9 +2019,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -2036,9 +2036,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -2053,9 +2053,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -2070,9 +2070,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -2087,9 +2087,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -2104,9 +2104,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -7572,9 +7572,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7585,32 +7585,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { diff --git a/frontend/ai.client/package.json b/frontend/ai.client/package.json index 403db58a..aaeeb08f 100644 --- a/frontend/ai.client/package.json +++ b/frontend/ai.client/package.json @@ -1,6 +1,6 @@ { "name": "ai.client", - "version": "1.0.1", + "version": "1.0.2", "scripts": { "ng": "ng", "start": "ng serve", @@ -73,6 +73,7 @@ "undici": ">=7.28.0 <8.0.0", "picomatch": ">=4.0.4", "vite": ">=8.0.16", + "esbuild": ">=0.28.1", "dompurify": ">=3.4.0", "lodash-es": ">=4.18.0", "hono": ">=4.12.25", diff --git a/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.spec.ts b/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.spec.ts new file mode 100644 index 00000000..4b016980 --- /dev/null +++ b/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.spec.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { detectAwsServiceFromUrl, extractAwsRegionFromUrl } from './admin-tool.model'; + +/** + * Regression tests for CodeQL js/regex/missing-regexp-anchor (alerts #670/#671). + * + * AWS-endpoint detection must match the URL *host* only. Previously the regexes + * ran unanchored against the whole URL string, so an AWS marker smuggled into a + * path/query/fragment/userinfo component β€” or an AWS suffix used as a + * non-terminal label of an attacker domain β€” was misclassified as an AWS host. + * At signing time that would attach SigV4 IAM credentials to a request bound for + * a non-AWS host. + */ +describe('admin-tool.model AWS URL detection (host-anchored)', () => { + describe('legitimate AWS endpoints are still detected', () => { + it('detects services', () => { + expect(detectAwsServiceFromUrl('https://x.lambda-url.us-west-2.on.aws/mcp')).toBe('lambda'); + expect(detectAwsServiceFromUrl('https://x.execute-api.us-east-1.amazonaws.com/p')).toBe( + 'execute-api', + ); + expect( + detectAwsServiceFromUrl('https://g.bedrock-agentcore.eu-west-1.amazonaws.com/'), + ).toBe('bedrock-agentcore'); + }); + + it('extracts regions (including with an explicit port)', () => { + expect(extractAwsRegionFromUrl('https://x.lambda-url.ap-south-1.on.aws/mcp')).toBe( + 'ap-south-1', + ); + expect( + extractAwsRegionFromUrl('https://x.execute-api.us-east-2.amazonaws.com:443/p'), + ).toBe('us-east-2'); + }); + }); + + describe('spoofed URLs must NOT be treated as AWS', () => { + const spoofed: [string, string][] = [ + ['marker in query', 'https://evil.example/?x=.execute-api.us-east-1.amazonaws.com'], + ['marker in path', 'https://evil.example/.lambda-url.us-east-1.on.aws/mcp'], + [ + 'aws suffix as non-terminal label', + 'https://x.execute-api.us-east-1.amazonaws.com.evil.example/mcp', + ], + ['marker in userinfo', 'https://.execute-api.us-east-1.amazonaws.com@evil.example/mcp'], + ['marker in fragment', 'https://evil.example/#.bedrock-agentcore.us-east-1.amazonaws.com'], + ]; + + it.each(spoofed)('detectAwsServiceFromUrl returns "" for %s', (_desc, url) => { + expect(detectAwsServiceFromUrl(url)).toBe(''); + }); + + it.each(spoofed)('extractAwsRegionFromUrl returns "" for %s', (_desc, url) => { + expect(extractAwsRegionFromUrl(url)).toBe(''); + }); + }); + + describe('non-AWS / unparseable inputs', () => { + it('returns "" for plain domains and empty input', () => { + expect(detectAwsServiceFromUrl('https://mcp.example.com/mcp')).toBe(''); + expect(detectAwsServiceFromUrl('')).toBe(''); + expect(extractAwsRegionFromUrl('https://mcp.example.com/mcp')).toBe(''); + expect(extractAwsRegionFromUrl('not a url')).toBe(''); + }); + }); +}); diff --git a/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.ts b/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.ts index 6fdab805..db823f03 100644 --- a/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.ts +++ b/frontend/ai.client/src/app/admin/tools/models/admin-tool.model.ts @@ -403,10 +403,33 @@ export const TOOL_STATUSES: { value: ToolStatus; label: string }[] = [ * fill) rather than defaulting to `'lambda'` β€” the backend's last-resort * default is only appropriate at signing time. */ +/** + * Lowercased hostname of `url`, or `''` if it can't be parsed. + * + * AWS-endpoint detection must match the *host* only. Testing an unanchored + * regex against the whole URL string (CodeQL js/regex/missing-regexp-anchor) + * lets a marker in a path/query β€” e.g. + * `https://evil.example/?x=.execute-api.us-east-1.amazonaws.com` β€” masquerade + * as an AWS host, which at signing time would attach SigV4 IAM credentials to + * a request bound for a non-AWS host. Parsing the host and anchoring the + * suffix (`$`) prevents that. + */ +function awsHostname(url: string): string { + for (const candidate of [url, `https://${url}`]) { + try { + return new URL(candidate).hostname.toLowerCase(); + } catch { + // try the next candidate (handles scheme-less inputs) + } + } + return ''; +} + export function detectAwsServiceFromUrl(url: string): string { - if (/\.lambda-url\.[a-z0-9-]+\.on\.aws/.test(url)) return 'lambda'; - if (/\.execute-api\.[a-z0-9-]+\.amazonaws\.com/.test(url)) return 'execute-api'; - if (/\.bedrock-agentcore\.[a-z0-9-]+\.amazonaws\.com/.test(url)) return 'bedrock-agentcore'; + const host = awsHostname(url); + if (/\.lambda-url\.[a-z0-9-]+\.on\.aws$/.test(host)) return 'lambda'; + if (/\.execute-api\.[a-z0-9-]+\.amazonaws\.com$/.test(host)) return 'execute-api'; + if (/\.bedrock-agentcore\.[a-z0-9-]+\.amazonaws\.com$/.test(host)) return 'bedrock-agentcore'; return ''; } @@ -415,9 +438,10 @@ export function detectAwsServiceFromUrl(url: string): string { * doesn't encode one. Mirrors the backend `extract_region_from_url`. */ export function extractAwsRegionFromUrl(url: string): string { + const host = awsHostname(url); const match = - url.match(/\.lambda-url\.([a-z0-9-]+)\.on\.aws/) ?? - url.match(/\.execute-api\.([a-z0-9-]+)\.amazonaws\.com/) ?? - url.match(/\.bedrock-agentcore\.([a-z0-9-]+)\.amazonaws\.com/); + host.match(/\.lambda-url\.([a-z0-9-]+)\.on\.aws$/) ?? + host.match(/\.execute-api\.([a-z0-9-]+)\.amazonaws\.com$/) ?? + host.match(/\.bedrock-agentcore\.([a-z0-9-]+)\.amazonaws\.com$/); return match ? match[1] : ''; } diff --git a/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.spec.ts b/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.spec.ts index 5b643a03..8b00d6f1 100644 --- a/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.spec.ts +++ b/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.spec.ts @@ -4,6 +4,7 @@ import { signal } from '@angular/core'; import { FETCH_EVENT_SOURCE, PreviewChatService } from './preview-chat.service'; import { SessionService as BffSessionService } from '../../../auth/session.service'; import { ConfigService } from '../../../services/config.service'; +import { ToolService } from '../../../services/tool/tool.service'; describe('PreviewChatService', () => { let service: PreviewChatService; @@ -32,12 +33,17 @@ describe('PreviewChatService', () => { appApiUrl: signal('http://localhost:8000'), }; + const toolServiceMock = { + getEnabledToolIds: vi.fn().mockReturnValue(['tool1', 'tool2']), + }; + TestBed.configureTestingModule({ providers: [ PreviewChatService, { provide: FETCH_EVENT_SOURCE, useValue: mockFetch }, { provide: BffSessionService, useValue: bffSessionMock }, { provide: ConfigService, useValue: configServiceMock }, + { provide: ToolService, useValue: toolServiceMock }, ], }); @@ -108,6 +114,16 @@ describe('PreviewChatService', () => { expect(service.sessionId()).toMatch(/^preview-/); }); + it('forwards the owner enabled tools so the preview can use tools', async () => { + await service.sendMessage('Hello', 'assistant-1', 'Test instructions'); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const init = mockFetch.mock.calls[0][1]!; + const body = JSON.parse(init.body as string); + expect(body.enabled_tools).toEqual(['tool1', 'tool2']); + expect(body.rag_assistant_id).toBe('assistant-1'); + }); + it('attaches the CSRF header from the BFF SessionService when present', async () => { bffSession.csrfHeaders.mockReturnValue({ 'X-CSRF-Token': 'tok-xyz' }); diff --git a/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.ts b/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.ts index 307e5c61..23846158 100644 --- a/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.ts +++ b/frontend/ai.client/src/app/assistants/assistant-form/services/preview-chat.service.ts @@ -4,12 +4,20 @@ import { fetchEventSource } from '@microsoft/fetch-event-source'; import type { EventSourceMessage, FetchEventSourceInit } from '@microsoft/fetch-event-source'; import { SessionService as BffSessionService } from '../../../auth/session.service'; import { ConfigService } from '../../../services/config.service'; -import { Message } from '../../../session/services/models/message.model'; +import { ToolService } from '../../../services/tool/tool.service'; +import { + Message, + type ContentBlock, + type ToolUseData, + type ToolResultContent, +} from '../../../session/services/models/message.model'; import { PREVIEW_SESSION_PREFIX } from '../../../shared/constants/session.constants'; import { processStreamEvent, type StreamParserCallbacks, type ContentBlockDeltaEvent, + type ToolUseEvent, + type ToolResultEventData, } from '../../../shared/utils/stream-parser'; /** @@ -39,14 +47,16 @@ export const FETCH_EVENT_SOURCE = new InjectionToken('FETCH_EV * Preview sessions use the `preview-{uuid}` session ID format which the backend recognizes * and skips persistence for. * - * For preview, we only need basic text streaming - tool use, citations, and other advanced - * features are not critical for testing assistant instructions. + * The preview streams text and tool use so owners can test the assistant exactly as a + * consumer would experience it (assistants can use the owner's enabled tools). Citations + * and other advanced features remain out of scope for the preview. */ @Injectable() export class PreviewChatService { private bffSession = inject(BffSessionService); private config = inject(ConfigService); private fetchEventSource = inject(FETCH_EVENT_SOURCE); + private toolService = inject(ToolService); // Local state signals (isolated from global ChatStateService) private readonly messagesSignal = signal([]); @@ -57,7 +67,14 @@ export class PreviewChatService { // Abort controller for cancellation private abortController: AbortController | null = null; - private currentMessageBuilder: { id: string; content: string } | null = null; + // Builds the streaming assistant message as an ordered list of content blocks + // (text interleaved with tool use), mirroring the main chat so the shared + // message-list renders tool cards in the preview. `activeTextIndex` points at + // the text block currently accumulating deltas, or -1 when the next text delta + // should open a fresh block (e.g. right after a tool block). + private currentMessageBuilder: + | { id: string; blocks: ContentBlock[]; activeTextIndex: number } + | null = null; // Public readonly signals readonly messages = this.messagesSignal.asReadonly(); @@ -80,9 +97,9 @@ export class PreviewChatService { }, onContentBlockDelta: (data: ContentBlockDeltaEvent) => { - // Only handle text deltas + // Only handle text deltas; tool input arrives whole via onToolUse. if (data.text && this.currentMessageBuilder) { - this.currentMessageBuilder.content += data.text; + this.appendText(data.text); this.updateCurrentMessage(); } }, @@ -106,19 +123,13 @@ export class PreviewChatService { (data as { message?: string; error?: string })?.error || 'An error occurred'; - if (this.currentMessageBuilder) { - this.currentMessageBuilder.content = `Error: ${errorMessage}`; - this.updateCurrentMessage(); - } + this.setErrorMessage(errorMessage); this.loadingSignal.set(false); this.streamingMessageIdSignal.set(null); }, onStreamError: (data) => { - if (this.currentMessageBuilder) { - this.currentMessageBuilder.content = `Error: ${data.message}`; - this.updateCurrentMessage(); - } + this.setErrorMessage(data.message); this.loadingSignal.set(false); this.streamingMessageIdSignal.set(null); }, @@ -127,11 +138,19 @@ export class PreviewChatService { console.warn('Preview chat parse error:', message); }, + onToolUse: (data: ToolUseEvent) => { + this.appendToolUse(data); + this.updateCurrentMessage(); + }, + + onToolResult: (data: ToolResultEventData) => { + this.applyToolResult(data); + this.updateCurrentMessage(); + }, + // Unused callbacks for preview - just ignore these events onContentBlockStart: () => {}, onContentBlockStop: () => {}, - onToolUse: () => {}, - onToolResult: () => {}, onToolProgress: () => {}, onMetadata: () => {}, onReasoning: () => {}, @@ -171,7 +190,7 @@ export class PreviewChatService { // Create placeholder for assistant response const assistantMessageId = `msg-${this.sessionIdSignal()}-${this.messagesSignal().length}`; - this.currentMessageBuilder = { id: assistantMessageId, content: '' }; + this.currentMessageBuilder = { id: assistantMessageId, blocks: [], activeTextIndex: -1 }; const assistantMsg: Message = { id: assistantMessageId, role: 'assistant', @@ -210,7 +229,9 @@ export class PreviewChatService { rag_assistant_id: assistantId, system_prompt: liveInstructions || null, // Send live form instructions for preview model_id: null, // Use default model - enabled_tools: [], // No tools in preview + // Forward the owner's enabled tools so the preview exercises tools the + // same way a consumer chat does. + enabled_tools: this.toolService.getEnabledToolIds(), }; if (fileUploadIds && fileUploadIds.length > 0) { requestBody['file_upload_ids'] = fileUploadIds; @@ -274,21 +295,102 @@ export class PreviewChatService { } /** - * Update the current assistant message in the messages array + * Append streamed text to the active text block, opening a new one if the + * previous block was a tool use (so text and tools render in order). + */ + private appendText(text: string): void { + const builder = this.currentMessageBuilder; + if (!builder) { + return; + } + if (builder.activeTextIndex < 0) { + builder.blocks.push({ type: 'text', text: '' }); + builder.activeTextIndex = builder.blocks.length - 1; + } + const block = builder.blocks[builder.activeTextIndex]; + block.text = (block.text ?? '') + text; + } + + /** + * Append a tool-use content block. Subsequent text deltas start a fresh text + * block so the tool card renders between the surrounding prose. + */ + private appendToolUse(data: ToolUseEvent): void { + const builder = this.currentMessageBuilder; + if (!builder) { + return; + } + const toolUse = data.tool_use; + let input: Record = {}; + try { + input = toolUse.input ? JSON.parse(toolUse.input) : {}; + } catch { + // Tool input JSON may be malformed mid-stream; fall back to empty object. + input = {}; + } + const toolUseData: ToolUseData = { + toolUseId: toolUse.tool_use_id, + name: toolUse.name, + input, + status: 'pending', + }; + builder.blocks.push({ type: 'toolUse', toolUse: toolUseData }); + builder.activeTextIndex = -1; + } + + /** + * Attach a tool result to its matching tool-use block. Replaces the block + * (new object references) so OnPush message rendering picks up the change. + */ + private applyToolResult(data: ToolResultEventData): void { + const builder = this.currentMessageBuilder; + if (!builder) { + return; + } + const result = data.tool_result; + const index = builder.blocks.findIndex( + (block) => + block.type === 'toolUse' && + (block.toolUse as ToolUseData | undefined)?.toolUseId === result.toolUseId, + ); + if (index < 0) { + return; + } + const existing = builder.blocks[index].toolUse as ToolUseData; + const status = result.status === 'error' ? 'error' : 'success'; + builder.blocks[index] = { + type: 'toolUse', + toolUse: { + ...existing, + result: { + content: (result.content ?? []) as unknown as ToolResultContent[], + status, + }, + status: status === 'error' ? 'error' : 'complete', + }, + }; + } + + /** + * Update the current assistant message in the messages array. Rebuilds the + * content blocks with fresh object references so OnPush change detection in + * the shared message-list re-renders streaming text and tool cards. */ private updateCurrentMessage(): void { if (!this.currentMessageBuilder) { return; } - const { id, content } = this.currentMessageBuilder; + const { id, blocks } = this.currentMessageBuilder; + const content: ContentBlock[] = + blocks.length > 0 ? blocks.map((block) => ({ ...block })) : [{ type: 'text', text: '' }]; this.messagesSignal.update((msgs) => { const index = msgs.findIndex((m) => m.id === id); if (index >= 0) { const updated = [...msgs]; updated[index] = { ...updated[index], - content: [{ type: 'text', text: content }], + content, }; return updated; } @@ -296,6 +398,19 @@ export class PreviewChatService { }); } + /** + * Replace the in-flight assistant message with an error notice. + */ + private setErrorMessage(message: string): void { + const builder = this.currentMessageBuilder; + if (!builder) { + return; + } + builder.blocks = [{ type: 'text', text: `Error: ${message}` }]; + builder.activeTextIndex = 0; + this.updateCurrentMessage(); + } + /** * Handle errors during streaming */ @@ -305,8 +420,7 @@ export class PreviewChatService { this.streamingMessageIdSignal.set(null); if (this.currentMessageBuilder) { - this.currentMessageBuilder.content = `Error: ${error.message}`; - this.updateCurrentMessage(); + this.setErrorMessage(error.message); this.currentMessageBuilder = null; } } diff --git a/frontend/ai.client/src/app/session/services/chat/chat-request.service.spec.ts b/frontend/ai.client/src/app/session/services/chat/chat-request.service.spec.ts index 06388d27..c6311026 100644 --- a/frontend/ai.client/src/app/session/services/chat/chat-request.service.spec.ts +++ b/frontend/ai.client/src/app/session/services/chat/chat-request.service.spec.ts @@ -117,7 +117,9 @@ describe('ChatRequestService', () => { const sent = mockChatHttpService.sendChatRequest.mock.calls[0][0]; expect('agent_type' in sent).toBe(false); expect('enabled_skills' in sent).toBe(false); - expect(sent['enabled_tools']).toEqual([]); + // Assistants forward the user's tool selection (skills mode is excluded for + // assistant turns), so the raw tool picker selection rides along. + expect(sent['enabled_tools']).toEqual(['tool1', 'tool2']); }); it('should include assistant ID in request', async () => { @@ -130,13 +132,13 @@ describe('ChatRequestService', () => { ); }); - it('overrides enabled_tools to [] when an assistant ID is set (KB-only consumer chat)', async () => { + it('forwards the user tool selection when an assistant ID is set (assistants can use tools)', async () => { await service.submitChatRequest('Hello', 'session1', undefined, 'assistant1'); expect(mockChatHttpService.sendChatRequest).toHaveBeenCalledWith( expect.objectContaining({ rag_assistant_id: 'assistant1', - enabled_tools: [], + enabled_tools: ['tool1', 'tool2'], }) ); }); diff --git a/frontend/ai.client/src/app/session/services/chat/chat-request.service.ts b/frontend/ai.client/src/app/session/services/chat/chat-request.service.ts index ca8f1540..3b02a18b 100644 --- a/frontend/ai.client/src/app/session/services/chat/chat-request.service.ts +++ b/frontend/ai.client/src/app/session/services/chat/chat-request.service.ts @@ -256,8 +256,6 @@ export class ChatRequestService implements OnDestroy { // AgentCore Runtime's internal 'assistant_id' field handling (causes 424 error) if (assistantId) { requestObject['rag_assistant_id'] = assistantId; - // Assistants are KB-grounded with no external tools. Backend enforces this too. - requestObject['enabled_tools'] = []; } else { // Forward the active conversation mode for non-assistant turns. The // assistant path is intentionally excluded server-side too β€” assistants diff --git a/infrastructure/lib/constructs/agentcore/browser-construct.ts b/infrastructure/lib/constructs/agentcore/browser-construct.ts index 73c98b6e..54d6568c 100644 --- a/infrastructure/lib/constructs/agentcore/browser-construct.ts +++ b/infrastructure/lib/constructs/agentcore/browser-construct.ts @@ -1,6 +1,5 @@ import * as bedrock from 'aws-cdk-lib/aws-bedrockagentcore'; import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/agentcore/code-interpreter-construct.ts b/infrastructure/lib/constructs/agentcore/code-interpreter-construct.ts index da5fac53..87ba37e6 100644 --- a/infrastructure/lib/constructs/agentcore/code-interpreter-construct.ts +++ b/infrastructure/lib/constructs/agentcore/code-interpreter-construct.ts @@ -1,6 +1,5 @@ import * as bedrock from 'aws-cdk-lib/aws-bedrockagentcore'; import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/app-api/app-api-environment.ts b/infrastructure/lib/constructs/app-api/app-api-environment.ts index f94e1fe9..80c7ffb0 100644 --- a/infrastructure/lib/constructs/app-api/app-api-environment.ts +++ b/infrastructure/lib/constructs/app-api/app-api-environment.ts @@ -8,7 +8,6 @@ */ import * as ssm from 'aws-cdk-lib/aws-ssm'; -import { Construct } from 'constructs'; import { AppConfig, buildCorsOrigins } from '../../config'; import { PlatformComputeRefs } from '../platform-compute-refs'; diff --git a/infrastructure/lib/constructs/app-api/app-api-iam-grants.ts b/infrastructure/lib/constructs/app-api/app-api-iam-grants.ts index ba837e18..32fce027 100644 --- a/infrastructure/lib/constructs/app-api/app-api-iam-grants.ts +++ b/infrastructure/lib/constructs/app-api/app-api-iam-grants.ts @@ -19,7 +19,6 @@ */ import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { AppConfig } from '../../config'; diff --git a/infrastructure/lib/constructs/app-api/app-api-service-construct.ts b/infrastructure/lib/constructs/app-api/app-api-service-construct.ts index ea79f1fa..1e5eac31 100644 --- a/infrastructure/lib/constructs/app-api/app-api-service-construct.ts +++ b/infrastructure/lib/constructs/app-api/app-api-service-construct.ts @@ -9,7 +9,7 @@ import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as path from 'path'; import { Construct } from 'constructs'; -import { AppConfig, getResourceName, buildCorsOrigins } from '../../config'; +import { AppConfig, getResourceName } from '../../config'; import { resolveAppApiParams, buildAppApiEnvironment } from './app-api-environment'; import { PlatformComputeRefs } from '../platform-compute-refs'; import { grantAppApiPermissions } from './app-api-iam-grants'; @@ -195,7 +195,7 @@ export class AppApiServiceConstruct extends Construct { environment['SAGEMAKER_SUBNET_IDS'] = props.sagemakerPrivateSubnetIds; // ── Container definition ── - const container = taskDefinition.addContainer('AppApiContainer', { + taskDefinition.addContainer('AppApiContainer', { containerName: 'app-api', image: ecs.ContainerImage.fromRegistry(appApiImageUri), logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'app-api', logGroup }), diff --git a/infrastructure/lib/constructs/artifacts/artifacts-distribution-construct.ts b/infrastructure/lib/constructs/artifacts/artifacts-distribution-construct.ts index fa38d6d3..d2ffa100 100644 --- a/infrastructure/lib/constructs/artifacts/artifacts-distribution-construct.ts +++ b/infrastructure/lib/constructs/artifacts/artifacts-distribution-construct.ts @@ -5,7 +5,6 @@ import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/fine-tuning/sagemaker-execution-role-construct.ts b/infrastructure/lib/constructs/fine-tuning/sagemaker-execution-role-construct.ts index c5c05120..0a8e82a3 100644 --- a/infrastructure/lib/constructs/fine-tuning/sagemaker-execution-role-construct.ts +++ b/infrastructure/lib/constructs/fine-tuning/sagemaker-execution-role-construct.ts @@ -2,7 +2,6 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; @@ -49,8 +48,7 @@ export class SageMakerExecutionRoleConstruct extends Construct { ) { super(scope, id); - const { config, dataBucket, jobsTable, vpc, privateSubnetIdsString } = - props; + const { config, dataBucket, jobsTable, vpc } = props; // Keep an explicit, stable roleName for consistency with the other // execution roles and to avoid replacing the role (and the dependent diff --git a/infrastructure/lib/constructs/identity/artifact-render-token-secret-construct.ts b/infrastructure/lib/constructs/identity/artifact-render-token-secret-construct.ts index 831b86b9..b08218cd 100644 --- a/infrastructure/lib/constructs/identity/artifact-render-token-secret-construct.ts +++ b/infrastructure/lib/constructs/identity/artifact-render-token-secret-construct.ts @@ -1,5 +1,4 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName, getRemovalPolicy } from '../../config'; diff --git a/infrastructure/lib/constructs/identity/auth-secret-construct.ts b/infrastructure/lib/constructs/identity/auth-secret-construct.ts index 7ff37b5e..3a06ff81 100644 --- a/infrastructure/lib/constructs/identity/auth-secret-construct.ts +++ b/infrastructure/lib/constructs/identity/auth-secret-construct.ts @@ -1,5 +1,4 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName, getRemovalPolicy } from '../../config'; diff --git a/infrastructure/lib/constructs/identity/bff-cookie-key-construct.ts b/infrastructure/lib/constructs/identity/bff-cookie-key-construct.ts index 723f6638..6727385f 100644 --- a/infrastructure/lib/constructs/identity/bff-cookie-key-construct.ts +++ b/infrastructure/lib/constructs/identity/bff-cookie-key-construct.ts @@ -1,6 +1,5 @@ import * as kms from 'aws-cdk-lib/aws-kms'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName, getRemovalPolicy } from '../../config'; diff --git a/infrastructure/lib/constructs/identity/platform-identity-construct.ts b/infrastructure/lib/constructs/identity/platform-identity-construct.ts index 2397886b..fbbda550 100644 --- a/infrastructure/lib/constructs/identity/platform-identity-construct.ts +++ b/infrastructure/lib/constructs/identity/platform-identity-construct.ts @@ -1,5 +1,4 @@ import * as bedrock from 'aws-cdk-lib/aws-bedrockagentcore'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/identity/voice-ticket-construct.ts b/infrastructure/lib/constructs/identity/voice-ticket-construct.ts index 50cea108..bf86827e 100644 --- a/infrastructure/lib/constructs/identity/voice-ticket-construct.ts +++ b/infrastructure/lib/constructs/identity/voice-ticket-construct.ts @@ -1,6 +1,5 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName, getRemovalPolicy } from '../../config'; diff --git a/infrastructure/lib/constructs/inference-api/inference-api-iam-roles.ts b/infrastructure/lib/constructs/inference-api/inference-api-iam-roles.ts index 20095559..f4318dbf 100644 --- a/infrastructure/lib/constructs/inference-api/inference-api-iam-roles.ts +++ b/infrastructure/lib/constructs/inference-api/inference-api-iam-roles.ts @@ -7,7 +7,6 @@ */ import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/mcp-sandbox/mcp-sandbox-distribution-construct.ts b/infrastructure/lib/constructs/mcp-sandbox/mcp-sandbox-distribution-construct.ts index 4a9408af..11f351d0 100644 --- a/infrastructure/lib/constructs/mcp-sandbox/mcp-sandbox-distribution-construct.ts +++ b/infrastructure/lib/constructs/mcp-sandbox/mcp-sandbox-distribution-construct.ts @@ -5,7 +5,6 @@ import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as fs from 'fs'; import * as path from 'path'; import { Construct } from 'constructs'; diff --git a/infrastructure/lib/constructs/network/alb-construct.ts b/infrastructure/lib/constructs/network/alb-construct.ts index df078ee3..cf631626 100644 --- a/infrastructure/lib/constructs/network/alb-construct.ts +++ b/infrastructure/lib/constructs/network/alb-construct.ts @@ -1,7 +1,6 @@ import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; diff --git a/infrastructure/lib/constructs/network/network-construct.ts b/infrastructure/lib/constructs/network/network-construct.ts index 45ea59a9..f2f39fb8 100644 --- a/infrastructure/lib/constructs/network/network-construct.ts +++ b/infrastructure/lib/constructs/network/network-construct.ts @@ -1,6 +1,4 @@ -import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { AppConfig, getResourceName } from '../../config'; @@ -65,17 +63,5 @@ export class NetworkConstruct extends Construct { // Export VPC CIDR to SSM - // Export Private Subnet IDs to SSM - const privateSubnetIds = this.vpc.privateSubnets - .map((subnet) => subnet.subnetId) - .join(','); - - // Export Public Subnet IDs to SSM - const publicSubnetIds = this.vpc.publicSubnets - .map((subnet) => subnet.subnetId) - .join(','); - - // Export Availability Zones to SSM - const availabilityZones = this.vpc.availabilityZones.join(','); } } diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json index 489998e7..f036d6db 100644 --- a/infrastructure/package-lock.json +++ b/infrastructure/package-lock.json @@ -1,12 +1,12 @@ { "name": "infrastructure", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "infrastructure", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "aws-cdk-lib": "2.260.0", "constructs": "10.6.0" diff --git a/infrastructure/package.json b/infrastructure/package.json index 08bd17f5..3685a3e7 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -1,6 +1,6 @@ { "name": "infrastructure", - "version": "1.0.1", + "version": "1.0.2", "bin": { "infrastructure": "bin/infrastructure.js" }, diff --git a/scripts/backend/test.sh b/scripts/backend/test.sh new file mode 100755 index 00000000..7203dfa0 --- /dev/null +++ b/scripts/backend/test.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -euo pipefail + +# scripts/backend/test.sh β€” run the backend (App API) pytest suite WITH coverage. +# +# Ported from the pre-#396 scripts/stack-app-api/test.sh during the +# platform-as-bootstrap refactor: the per-stack script dirs (stack-app-api, +# stack-frontend, …) were deleted and replaced by the single-stack layout, +# but nightly.yml was never repointed. repo-shape.test.ts now FORBIDS +# recreating scripts/stack-*, so the coverage variant lives here instead. +# +# This is the COVERAGE variant used by nightly.yml's "Test Backend with +# Coverage" job (it emits backend/coverage.json + backend/htmlcov/ which the +# workflow uploads as the `backend-coverage` artifact and feeds to the +# coverage-analysis jobs). The plain, no-coverage gate used by +# nightly-deploy-pipeline.yml / backend.yml stays inline (`uv run pytest`). +# +# Runs on a fresh, isolated GitHub runner: the uv *cache* (~/.cache/uv + +# backend/.venv) is restored by the install-backend job, but the uv *binary* +# is not, so this script installs uv if it's missing. + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BACKEND_DIR="${PROJECT_ROOT}/backend" + +# Logging functions +log_info() { + echo "[INFO] $1" +} + +log_error() { + echo "[ERROR] $1" >&2 +} + +log_success() { + echo "[SUCCESS] $1" +} + +main() { + log_info "Running App API tests..." + + # Change to backend directory + cd "${BACKEND_DIR}" + log_info "Working directory: $(pwd)" + + # Install uv if not present (the runner is fresh; only the cache persists) + if ! command -v uv &> /dev/null; then + log_info "Installing uv..." + curl -LsSf https://astral.sh/uv/0.7.12/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + fi + + # Sync dependencies from lock file (includes dev deps for testing) + log_info "Syncing dependencies from uv.lock..." + uv sync --frozen --extra agentcore --extra dev + + # Verify installation + log_info "Verifying installation..." + uv run python -c "import fastapi; import uvicorn; print('Core dependencies installed')" + + # Run tests + log_info "Executing tests..." + + if [ ! -d "tests" ]; then + log_info "No tests/ directory found. Skipping tests." + log_success "App API tests completed successfully!" + return 0 + fi + + # Set PYTHONPATH explicitly + export PYTHONPATH="${BACKEND_DIR}/src:${PYTHONPATH:-}" + log_info "PYTHONPATH=${PYTHONPATH}" + + # Set dummy AWS credentials for tests + export AWS_DEFAULT_REGION=us-east-1 + export AWS_ACCESS_KEY_ID=testing + export AWS_SECRET_ACCESS_KEY=testing + + # Test import directly + log_info "Testing direct import..." + uv run python -c "from agents.main_agent.quota.checker import QuotaChecker; print('Direct import works')" + + # Run pytest with coverage (html + json + term reports) + log_info "Running pytest..." + uv run python -m pytest tests/ \ + -v \ + --tb=short \ + --color=yes \ + --disable-warnings \ + --cov=src \ + --cov-report=html \ + --cov-report=json \ + --cov-report=term + + log_success "App API tests completed successfully!" +} + +main "$@" diff --git a/scripts/frontend/install.sh b/scripts/frontend/install.sh new file mode 100755 index 00000000..bdae6ff0 --- /dev/null +++ b/scripts/frontend/install.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# scripts/frontend/install.sh β€” install Angular frontend + CDK infrastructure deps. +# +# Ported from the pre-#396 scripts/stack-frontend/install.sh during the +# platform-as-bootstrap refactor (the stack-* script dirs were removed and +# repo-shape.test.ts now forbids recreating them). Used by nightly.yml's +# install-frontend job, which caches frontend/ai.client/node_modules. +# +# Installs, in order: +# 1. Angular frontend dependencies (npm ci) +# 2. CDK infrastructure dependencies (npm ci) + +set -euo pipefail + +# Get the repository root directory +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +FRONTEND_DIR="${REPO_ROOT}/frontend/ai.client" +INFRA_DIR="${REPO_ROOT}/infrastructure" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +# =========================================================== +# Install Frontend Dependencies +# =========================================================== + +# Check if frontend directory exists +if [ ! -d "${FRONTEND_DIR}" ]; then + log_error "Frontend directory not found: ${FRONTEND_DIR}" + exit 1 +fi + +# Check if package.json exists +if [ ! -f "${FRONTEND_DIR}/package.json" ]; then + log_error "package.json not found in ${FRONTEND_DIR}" + exit 1 +fi + +log_info "Installing frontend dependencies..." +log_info "Frontend directory: ${FRONTEND_DIR}" + +# Change to frontend directory +cd "${FRONTEND_DIR}" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + log_error "Node.js is not installed. Please install Node.js first." + log_error "Run: scripts/common/install-deps.sh" + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + log_error "npm is not installed. Please install npm first." + exit 1 +fi + +# Display Node.js and npm versions +log_info "Node.js version: $(node --version)" +log_info "npm version: $(npm --version)" + +# Use npm ci for clean, reproducible builds +# npm ci is faster and more reliable than npm install in CI/CD environments +if [ -f "package-lock.json" ]; then + log_info "Running npm ci (clean install from package-lock.json)..." + npm ci +else + log_error "package-lock.json not found. Cannot run npm ci." + exit 1 +fi + +log_success "Frontend dependencies installed successfully!" + +# Display Angular CLI version if available +if [ -f "node_modules/.bin/ng" ]; then + log_info "Angular CLI version: $(./node_modules/.bin/ng version --version 2>/dev/null || echo 'unknown')" +fi + +# =========================================================== +# Install CDK Dependencies +# =========================================================== + +log_info "Installing CDK dependencies..." +cd "${INFRA_DIR}" + +if [ -f "package-lock.json" ]; then + log_info "Running npm ci (clean install from package-lock.json)..." + npm ci +else + log_error "package-lock.json not found. Cannot run npm ci." + exit 1 +fi + +log_success "CDK dependencies installed successfully" diff --git a/scripts/frontend/test.sh b/scripts/frontend/test.sh new file mode 100755 index 00000000..53aacea8 --- /dev/null +++ b/scripts/frontend/test.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# scripts/frontend/test.sh β€” run the Angular test suite (headless) WITH coverage. +# +# Ported from the pre-#396 scripts/stack-frontend/test.sh during the +# platform-as-bootstrap refactor (the stack-* script dirs were removed and +# repo-shape.test.ts now forbids recreating them). Used by nightly.yml's +# "Test Frontend with Coverage" job, which uploads frontend/ai.client/coverage/. + +set -euo pipefail + +# Get the repository root directory +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +FRONTEND_DIR="${REPO_ROOT}/frontend/ai.client" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +# Check if frontend directory exists +if [ ! -d "${FRONTEND_DIR}" ]; then + log_error "Frontend directory not found: ${FRONTEND_DIR}" + exit 1 +fi + +# Check if node_modules exists +if [ ! -d "${FRONTEND_DIR}/node_modules" ]; then + log_error "node_modules not found. Please run install.sh first." + log_error "Run: scripts/frontend/install.sh" + exit 1 +fi + +log_info "Running frontend tests..." +log_info "Frontend directory: ${FRONTEND_DIR}" + +# Change to frontend directory +cd "${FRONTEND_DIR}" + +# Check if Angular CLI is available +if [ ! -f "node_modules/.bin/ng" ]; then + log_error "Angular CLI not found in node_modules. Please run install.sh first." + exit 1 +fi + +# Run tests in headless mode (no watch, single run) +# This is appropriate for CI/CD environments +log_info "Running: ng test --no-watch --coverage (filtering CSS warnings)" + +# Set environment variable for CI +export CI=true + +# Run tests with code coverage +# Angular uses Vitest which handles headless mode automatically in CI environments +# The CI=true environment variable triggers headless behavior +# Filter out jsdom CSS parsing warnings that clutter logs +# Use PIPESTATUS to preserve test exit code after grep filter +./node_modules/.bin/ng test --no-watch --coverage 2>&1 | grep -v "Could not parse CSS stylesheet" +TEST_EXIT_CODE=${PIPESTATUS[0]} + +if [ ${TEST_EXIT_CODE} -eq 0 ]; then + log_info "All tests passed successfully!" + + # Display coverage summary if available + if [ -f "coverage/index.html" ]; then + log_info "Coverage report generated: coverage/index.html" + fi + + # Display coverage summary from terminal output + if [ -d "coverage" ]; then + COVERAGE_DIR=$(find coverage -mindepth 1 -maxdepth 1 -type d | head -1) + if [ -n "${COVERAGE_DIR}" ] && [ -f "${COVERAGE_DIR}/coverage-summary.json" ]; then + log_info "Coverage summary available in: ${COVERAGE_DIR}/coverage-summary.json" + fi + fi +else + log_error "Tests failed with exit code: ${TEST_EXIT_CODE}" + exit ${TEST_EXIT_CODE} +fi diff --git a/tests/supply_chain/test_backup_coverage.py b/tests/supply_chain/test_backup_coverage.py index 514214c3..cf693eb7 100644 --- a/tests/supply_chain/test_backup_coverage.py +++ b/tests/supply_chain/test_backup_coverage.py @@ -11,8 +11,6 @@ Run with: pytest tests/supply_chain/test_backup_coverage.py -v """ -import ast -import os import re from pathlib import Path diff --git a/tests/supply_chain/test_env_var_contract.py b/tests/supply_chain/test_env_var_contract.py index 44da4ba8..932b8309 100644 --- a/tests/supply_chain/test_env_var_contract.py +++ b/tests/supply_chain/test_env_var_contract.py @@ -88,7 +88,7 @@ PY_ENV_RE = re.compile( r""" - os\.(?:environ\.get|getenv|environ\[) # os.environ.get / os.getenv / os.environ[ + os\.(?:environ\.get|getenv|environ\[) # os.environ.get / os.getenv / os.environ subscript \s*\(?\s* # optional whitespace + optional ( ['"] # opening quote ([A-Z][A-Z0-9_]+) # NAME @@ -97,10 +97,9 @@ re.VERBOSE, ) -# The EnvVars class in constants.py defines indirected env var -# references like: -# class EnvVars: -# DYNAMODB_QUOTA_EVENTS_TABLE = "DYNAMODB_QUOTA_EVENTS_TABLE" +# The EnvVars class in constants.py defines indirected env var references: +# each attribute is named identically to the string literal it holds (for +# example, the DYNAMODB_QUOTA_EVENTS_TABLE attribute maps to that same name). ENV_VARS_CLASS_RE = re.compile( r""" ^ # line start (in MULTILINE)