From 11b574df095cde98683a99cba8362ddbb4bb9089 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Thu, 27 Aug 2026 16:49:17 +0200 Subject: [PATCH 1/9] Security changes and MCP prompt tool choose --- .dockerignore | 47 +++ .github/workflows/bd_sca_scanner.yaml | 10 +- .github/workflows/hyperforge_workflow.yaml | 14 + HYPERFORGE.Dockerfile | 58 +-- SECURITY_SCAN_REPORT.md | 369 ++++++++++++++++++ .../external/src/hyperforge_external/agent.py | 36 +- .../src/hyperforge_external/config.py | 7 + agents/external/tests/test_external.py | 13 +- agents/http/src/hyperforge_http/agent.py | 29 +- agents/http/src/hyperforge_http/config.py | 8 +- agents/http/tests/test_http_static.py | 15 +- agents/mcp/src/hyperforge_mcp/agent.py | 12 +- .../mcp/src/hyperforge_mcp/config_driver.py | 9 +- agents/mcp/src/hyperforge_mcp/http.py | 18 +- agents/mcp/src/hyperforge_mcp/multi.py | 7 +- agents/mcp/src/hyperforge_mcp/tools.py | 15 +- agents/mcp/tests/test_mcp.py | 6 +- .../src/hyperforge_restricted/sandbox.py | 2 +- agents/restricted/tests/test_sandbox.py | 12 +- hyperforge/frontend/package-lock.json | 272 +++++++------ hyperforge/frontend/package.json | 10 +- hyperforge/frontend/src/stores/chat.ts | 3 +- hyperforge/frontend/src/utils/oauthUrl.ts | 11 + hyperforge/pyproject.toml | 3 +- hyperforge/src/hyperforge/api/app.py | 3 +- hyperforge/src/hyperforge/api/settings.py | 2 + .../src/hyperforge/api/v1/mcp_interaction.py | 43 +- hyperforge/src/hyperforge/interaction.py | 8 +- hyperforge/src/hyperforge/manager.py | 51 +++ hyperforge/src/hyperforge/standalone/app.py | 52 ++- hyperforge/src/hyperforge/standalone/oauth.py | 20 +- .../src/hyperforge/standalone/settings.py | 27 +- hyperforge/src/hyperforge/utils/__init__.py | 42 +- hyperforge/src/hyperforge/utils/http.py | 143 +++++-- .../tests/standalone/test_standalone.py | 41 ++ .../tests/unit/arag/test_internal_routes.py | 15 + hyperforge/tests/unit/test_safe_http.py | 52 +++ uv.lock | 4 +- 38 files changed, 1180 insertions(+), 309 deletions(-) create mode 100644 .dockerignore create mode 100644 SECURITY_SCAN_REPORT.md create mode 100644 hyperforge/frontend/src/utils/oauthUrl.ts create mode 100644 hyperforge/tests/unit/arag/test_internal_routes.py create mode 100644 hyperforge/tests/unit/test_safe_http.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8fca9a01 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +.git +.github +.env +.env.* +!.env.example +*.pem +*.key +*.p12 +*.pfx +*.jks +*.keystore +*.creds +appsecrets +env.sh +storage-*.json + +.venv +**/.venv +node_modules +**/node_modules +__pycache__ +**/__pycache__ +*.py[cod] +.pytest_cache +**/.pytest_cache +.mypy_cache +**/.mypy_cache +.ruff_cache +**/.ruff_cache +.benchmarks +.coverage +.coverage.* +htmlcov +dist +build +site +*.egg-info + +**/tests +**/test +**/cassettes +.vscode +.idea +.DS_Store +*.log +*.bak +SECURITY_SCAN_REPORT.md diff --git a/.github/workflows/bd_sca_scanner.yaml b/.github/workflows/bd_sca_scanner.yaml index 6066ca39..0131b0e7 100644 --- a/.github/workflows/bd_sca_scanner.yaml +++ b/.github/workflows/bd_sca_scanner.yaml @@ -5,11 +5,15 @@ on: - main paths: - '**/*.lock' + - '**/package.json' + - '**/package-lock.json' pull_request: branches: - main paths: - '**/*.lock' + - '**/package.json' + - '**/package-lock.json' workflow_dispatch: jobs: @@ -30,9 +34,9 @@ jobs: id: calculate-detect-args run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "DETECT_ARGS=--detect.excluded.directories=.bridge --detect.included.detector.types=uv --detect.uv.dependency.groups.excluded=dev --detect.timeout=600 --detect.excluded.detector.types=GIT --detect.blackduck.rapid.compare.mode=BOM_COMPARE_STRICT" >> $GITHUB_OUTPUT + echo "DETECT_ARGS=--detect.excluded.directories=.bridge --detect.included.detector.types=uv,npm --detect.uv.dependency.groups.excluded=dev --detect.timeout=600 --detect.excluded.detector.types=GIT --detect.blackduck.rapid.compare.mode=BOM_COMPARE_STRICT" >> $GITHUB_OUTPUT else - echo "DETECT_ARGS=--detect.excluded.directories=.bridge --detect.included.detector.types=uv --detect.uv.dependency.groups.excluded=dev --detect.timeout=600 --detect.excluded.detector.types=GIT" >> $GITHUB_OUTPUT + echo "DETECT_ARGS=--detect.excluded.directories=.bridge --detect.included.detector.types=uv,npm --detect.uv.dependency.groups.excluded=dev --detect.timeout=600 --detect.excluded.detector.types=GIT" >> $GITHUB_OUTPUT fi - name: Run Black Duck SCA PR Scan @@ -49,6 +53,6 @@ jobs: blackducksca_scan_full: ${{ github.event_name == 'pull_request' && 'false' || 'true' }} project_directory: '.' blackducksca_scan_failure_severities: 'BLOCKER,CRITICAL,MAJOR' - detect_search_depth: 1 + detect_search_depth: 3 detect_args: ${{ steps.calculate-detect-args.outputs.DETECT_ARGS }} include_diagnostics: false diff --git a/.github/workflows/hyperforge_workflow.yaml b/.github/workflows/hyperforge_workflow.yaml index ed569306..47565dcf 100644 --- a/.github/workflows/hyperforge_workflow.yaml +++ b/.github/workflows/hyperforge_workflow.yaml @@ -47,6 +47,20 @@ jobs: - name: Install package dependencies with uv run: uv sync --group dev + - name: Set up Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: "22.22.0" + cache: npm + cache-dependency-path: hyperforge/frontend/package-lock.json + + - name: Audit and build frontend + working-directory: hyperforge/frontend + run: | + npm ci + npm audit --audit-level=moderate + npm run build + - name: Lint working-directory: hyperforge run: make lint diff --git a/HYPERFORGE.Dockerfile b/HYPERFORGE.Dockerfile index 689de342..9f9f39ba 100644 --- a/HYPERFORGE.Dockerfile +++ b/HYPERFORGE.Dockerfile @@ -1,22 +1,36 @@ -FROM python:3.14 AS build - -RUN pip install uv && apt update -y && apt install -y npm && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Install dependencies -RUN uv venv /app -ENV VIRTUAL_ENV=/app -COPY . /app/. -RUN uv sync --active --frozen --directory /app --compile-bytecode - -# -# Only copy the virtual env to the final image. -# -FROM python:3.14 -COPY --from=build /app /app -ENV PATH=/app/bin:$PATH - -# OCI metadata: links this image to the GitHub repo as a package, with source + license. -LABEL org.opencontainers.image.source="https://github.com/nuclia/hyperforge" \ - org.opencontainers.image.description="Agentic workflow on top of NUA" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.authors="Nuclia Team" \ No newline at end of file +FROM python:3.14.7-slim-trixie@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83 AS build + +COPY --from=ghcr.io/astral-sh/uv:0.10.8@sha256:88234bc9e09c2b2f6d176a3daf411419eb0370d450a08129257410de9cfafd2a /uv /usr/local/bin/uv + +RUN apt-get update \ + && apt-get install --no-install-recommends -y build-essential libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY pyproject.toml uv.lock ./ +COPY hyperforge/ hyperforge/ +COPY agents/ agents/ + +RUN uv venv /opt/venv \ + && VIRTUAL_ENV=/opt/venv uv sync --active --frozen --no-dev --no-editable --compile-bytecode + +FROM python:3.14.7-slim-trixie@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83 + +RUN apt-get update \ + && apt-get install --no-install-recommends -y libpq5 \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid 10001 hyperforge \ + && useradd --uid 10001 --gid 10001 --create-home hyperforge + +COPY --from=build --chown=10001:10001 /opt/venv /opt/venv + +ENV PATH="/opt/venv/bin:${PATH}" \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /home/hyperforge +USER 10001:10001 +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)"] +CMD ["hyperforge-api"] diff --git a/SECURITY_SCAN_REPORT.md b/SECURITY_SCAN_REPORT.md new file mode 100644 index 00000000..77e15362 --- /dev/null +++ b/SECURITY_SCAN_REPORT.md @@ -0,0 +1,369 @@ +# Security Assessment Report + +**Project:** Hyperforge +**Assessment date:** 2026-08-26 +**Revision reviewed:** `4c0d323606216447f795b7a463657e6d1dcf17e0` (`main`) plus the existing uncommitted changes listed under Scope +**Assessment type:** Static source, configuration, dependency, secret-pattern, and targeted test review + +> **Remediation update (2026-08-26):** All confirmed findings in this report have been addressed in the working tree. HF-01 through HF-09 are fixed pending deployment and release. The validation section below records the original scan; post-remediation checks include a clean npm audit, successful frontend build, lint/type checks, and passing targeted security and agent tests. Docker-dependent integration and image scans remain blocked by the unavailable local Docker daemon. + +## Executive Summary + +Hyperforge has two credential-disclosure paths that should be addressed before either affected service is reachable from an untrusted network. Standalone mode deliberately grants every request all roles while exposing endpoints that return and replace the complete agent configuration. The full API also mounts an unauthenticated internal inspection endpoint that returns decrypted driver configurations for a caller-selected account and agent. + +The outbound HTTP safeguard is a useful control, but it validates only IPv4 and performs DNS validation separately from connection establishment. This leaves IPv6 and DNS-rebinding SSRF paths, particularly serious when combined with standalone configuration access. Several request and response paths also buffer data without size limits, creating denial-of-service risk. + +The frontend lockfile contains six npm advisories (two high, one moderate, and three low). The two high advisories are not directly exploitable through the application behavior observed: Hyperforge uses only UUID v4, and PostCSS runs as a build dependency without processing user-submitted CSS. They should still be upgraded to restore a clean supply chain. + +No obvious committed production credential, private key, SQL injection, shell injection, unsafe YAML deserialization, or direct template XSS was confirmed in the current tracked tree. + +### Finding Summary + +| ID | Severity | Finding | Status | +|---|---|---|---| +| HF-01 | Critical | Standalone configuration and secrets are exposed without authentication | Remediated | +| HF-02 | High | Internal inspection endpoint returns decrypted credentials without authorization | Remediated | +| HF-03 | High | SSRF protection is bypassable through IPv6 or DNS rebinding | Remediated | +| HF-04 | High | Docker build can copy local secrets and Git metadata into the runtime image | Remediated | +| HF-05 | Medium | MCP request and response buffering permits memory exhaustion | Remediated | +| HF-06 | Medium | HTTP agents accept unbounded remote response bodies | Remediated | +| HF-07 | Medium | Runtime container executes as root | Remediated | +| HF-08 | Medium | Locked npm dependencies have known vulnerabilities | Remediated | +| HF-09 | Low | MCP client certificate temporary files are not deleted | Remediated | + +## Immediate Actions + +1. Prevent network access to standalone mode and `/api/internal/*` until HF-01 and HF-02 are fixed. +2. Rotate credentials if either endpoint has been exposed to an untrusted network; access logs alone cannot prove secrets were not read. +3. Require authenticated administrative authorization for standalone configuration access and redact every secret-bearing field. +4. Remove or strongly authenticate the internal inspection endpoint and never serialize decrypted driver secrets. +5. Treat configurable outbound URLs as SSRF-capable until DNS and connection validation are made atomic for IPv4 and IPv6. + +## Detailed Findings + +### HF-01: Standalone Configuration and Secrets Exposed Without Authentication + +**Severity:** Critical +**CWE:** CWE-306 (Missing Authentication), CWE-200 (Exposure of Sensitive Information) +**Affected component:** Standalone API/UI + +**Evidence** + +- `hyperforge/src/hyperforge/standalone/settings.py:34-35` binds standalone mode to `0.0.0.0:8080` by default. +- `hyperforge/src/hyperforge/standalone/app.py:91-149` grants every request all application roles. Optional bearer validation applies only to recognized MCP paths. +- `hyperforge/src/hyperforge/standalone/app.py:237-242` mounts the UI router under the open authentication backend. +- `hyperforge/src/hyperforge/standalone/ui_router.py:127-134` returns the complete in-memory configuration using `model_dump()`. +- `hyperforge/src/hyperforge/standalone/ui_router.py:137-177` accepts, activates, and persists a replacement configuration without an authorization check. +- `hyperforge/src/hyperforge/standalone/settings.py:120-126` allows every CORS origin by default, increasing browser-based reachability where the service is network-accessible. + +**Impact** + +An unauthenticated client can retrieve configured API keys, OAuth client secrets, authorization headers, certificates, prompts, and workflow definitions. The client can replace live behavior, persist malicious configuration, consume paid APIs, and configure outbound requests for data exfiltration or SSRF. Because the full configuration is serialized directly, the database API's encrypted-field redaction is not applied. + +**Attack path** + +1. Request `GET /api/v1/ui/config` from a reachable standalone instance. +2. Extract credentials and internal endpoint configuration from the response. +3. Submit a modified document to `PUT /api/v1/ui/config` to change active workflows or outbound integrations. + +**Remediation** + +- Bind standalone mode to loopback by default. +- Require strong authentication and a distinct administrator permission for every `/api/v1/ui/*` endpoint that reads or mutates configuration. +- Disable configuration mutation by default in production. +- Define sensitive fields centrally and redact them from all responses, including custom headers, certificates, MCP environment values, and driver-specific credentials. +- Restrict CORS to explicit trusted origins. Do not combine wildcard origins with credentialed requests. +- Add audit logging for configuration reads and writes without logging configuration values. + +### HF-02: Internal Inspection Endpoint Returns Decrypted Credentials + +**Severity:** High +**CWE:** CWE-862 (Missing Authorization), CWE-200 (Exposure of Sensitive Information) +**Affected component:** Full FastAPI service + +**Evidence** + +- `hyperforge/src/hyperforge/api/internal/inspect.py:13-29` exposes `GET /api/internal/v1/agent/{kbid}` with a caller-controlled `account` query parameter and no authorization decorator. +- `hyperforge/src/hyperforge/api/app.py:77-83` mounts the internal router in the same application. +- `hyperforge/src/hyperforge/db/agents.py:451-480` decrypts marked fields before returning each driver. +- `hyperforge/src/hyperforge/api/models.py:79-84` includes the full `DriverConfig` list in the response. +- The normal management endpoint uses `dump_without_encrypted_fields()` at `hyperforge/src/hyperforge/api/v1/agents.py:160`; the inspection route does not. + +**Impact** + +Any caller that can reach the endpoint and identify or enumerate an account and agent can obtain decrypted driver credentials. The account value is supplied by the caller rather than derived from an authenticated identity. + +**Remediation** + +- Prefer removing the endpoint from the public application and exposing equivalent diagnostics through an authenticated internal service boundary. +- If retained, require service authentication and account-scoped authorization. +- Return only an allowlisted diagnostic projection and always remove encrypted fields after decryption. +- Add regression tests proving anonymous, cross-account, and ordinary member requests cannot access the route. + +### HF-03: SSRF Protection Bypass Through IPv6 or DNS Rebinding + +**Severity:** High +**CWE:** CWE-918 (Server-Side Request Forgery) +**Affected component:** Shared outbound HTTP transport and configurable URL consumers + +**Evidence** + +- `hyperforge/src/hyperforge/utils/http.py:17-30` resolves only `socket.AF_INET`, so AAAA records are not inspected. +- `hyperforge/src/hyperforge/utils/http.py:32-41` validates DNS before delegating to `httpx`, which resolves the hostname again when connecting. +- `agents/http/src/hyperforge_http/agent.py:36-68`, `agents/external/src/hyperforge_external/agent.py:64-117`, and `agents/mcp/src/hyperforge_mcp/http.py:454-490` consume configurable URLs through this transport. +- `agents/mcp/src/hyperforge_mcp/http.py:454-490` follows redirects; every redirect is checked by a new transport request, but each check still has the resolution race. +- `hyperforge/src/hyperforge/standalone/oauth.py:30-48` retrieves a configured JWKS URL without the shared SSRF transport. + +**Impact** + +A hostname can pass the IPv4 check while resolving to loopback or private IPv6 for the actual connection. DNS rebinding can also return a public address during validation and an internal address during connection. An attacker with configuration control can target cloud metadata, loopback services, cluster APIs, or internal data stores. HF-01 makes this attack unauthenticated in reachable standalone deployments. + +**Remediation** + +- Resolve all A and AAAA records and reject every address that is not explicitly permitted and globally routable. +- Connect to a validated address without a second uncontrolled DNS lookup while preserving the original hostname for TLS SNI and certificate validation. +- Revalidate redirect targets and restrict URL schemes to `https` or an explicit allowlist. +- Disable environment-proxy inheritance unless intentionally configured. +- Apply the same transport and policy to JWKS, OAuth metadata, and every configurable outbound endpoint. +- Consider destination allowlists for production rather than relying only on address classification. + +### HF-04: Docker Build Can Embed Local Secrets and Git Metadata + +**Severity:** High +**CWE:** CWE-200 (Exposure of Sensitive Information) +**Affected component:** `HYPERFORGE.Dockerfile` + +**Evidence** + +- No `.dockerignore` is present. +- `HYPERFORGE.Dockerfile:8` executes `COPY . /app/.`. +- `HYPERFORGE.Dockerfile:15` copies all of `/app` into the final image despite the comment stating that only the virtual environment is copied. + +**Impact** + +Docker sends the whole local build context unless excluded. Ignored `.env` files, credentials, `.git` history, test cassettes, caches, and developer artifacts can therefore be copied into image layers and the final image. Removing a file in a later layer would not reliably remove it from earlier layers. + +**Remediation** + +- Add a restrictive `.dockerignore` covering `.git`, environment files, credentials, caches, tests, local virtual environments, build outputs, and editor files. +- Copy manifests first, install dependencies, then copy only required source and built frontend assets. +- Copy the actual virtual environment and required runtime files into the final stage rather than all `/app`. +- Build and scan the resulting image, then inspect its filesystem and layer history before release. + +### HF-05: Unbounded MCP Request and Response Buffering + +**Severity:** Medium +**CWE:** CWE-400 (Uncontrolled Resource Consumption) +**Affected component:** Streamable MCP HTTP endpoint + +**Evidence** + +- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:382-392` accumulates all response chunks in a list. +- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:394-407` reads the complete request body into memory. +- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:425-428` joins all chunks, requiring another contiguous allocation. +- Standalone mode makes the route unauthenticated unless MCP auth is configured for the exact recognized path. + +**Impact** + +Large request bodies or generated MCP responses can exhaust process memory. Concurrent requests amplify the effect and can terminate the service. + +**Remediation** + +- Reject oversized requests at the reverse proxy and application boundary before calling `request.body()`. +- Stream requests and responses using the supported MCP session manager where possible. +- Cap total response bytes, chunk count, request duration, and concurrent MCP operations. +- Add load tests covering compressed and chunked bodies. + +### HF-06: HTTP Agents Accept Unbounded Remote Responses + +**Severity:** Medium +**CWE:** CWE-400 (Uncontrolled Resource Consumption) +**Affected component:** HTTP and external agents + +**Evidence** + +- `agents/http/src/hyperforge_http/agent.py:38-68` downloads and decodes the complete response. +- `agents/external/src/hyperforge_external/agent.py:118-131` decodes, logs, and stores the complete response. +- `agents/mcp/src/hyperforge_mcp/http.py:459-463` permits a 200-second default MCP timeout. + +**Impact** + +A malicious or compromised remote server can return an arbitrarily large or slowly streamed body, consuming worker memory and time. External-agent responses may also create very large logs and expose remote response data to centralized logging. + +**Remediation** + +- Stream responses and stop after a configurable maximum compressed and decompressed byte count. +- Apply connect, read, write, pool, and total operation timeouts. +- Truncate error bodies and avoid logging complete response bodies or model context. + +### HF-07: Runtime Container Executes as Root + +**Severity:** Medium +**CWE:** CWE-250 (Execution with Unnecessary Privileges) +**Affected component:** `HYPERFORGE.Dockerfile` + +**Evidence** + +- `HYPERFORGE.Dockerfile:14-22` does not create or select a non-root runtime user. +- The image also has no explicit `ENTRYPOINT`, `CMD`, or `HEALTHCHECK`. +- Base image `python:3.14`, bootstrap `uv`, and apt-installed npm are not digest/version pinned at `HYPERFORGE.Dockerfile:1-3,14`. + +**Impact** + +Application compromise provides root privileges inside the container and increases the consequences of mounted files, permissive runtime settings, or container-engine weaknesses. Mutable inputs reduce build reproducibility. + +**Remediation** + +- Create a dedicated UID/GID, ensure runtime files are owned appropriately, and set `USER` in the final stage. +- Use a minimal pinned base image and pin bootstrap tooling. +- Set an explicit startup command and health check or document that the orchestrator supplies them. +- Run with a read-only root filesystem, dropped Linux capabilities, no privilege escalation, resource limits, and a restrictive seccomp/AppArmor profile. + +### HF-08: Locked npm Dependencies Have Known Vulnerabilities + +**Severity:** Medium application risk; upstream advisories include High +**CWE:** CWE-1104 (Use of Unmaintained Third-Party Components) +**Affected component:** `hyperforge/frontend/package-lock.json` + +**Evidence** + +`npm audit --json` reported two high, one moderate, and three low vulnerabilities: + +| Package | Locked version | Advisory severity | Reachability assessment | +|---|---:|---:|---| +| `uuid` | 9.0.1 | High | Vulnerability affects v3/v5/v6 buffer APIs; project imports only v4 at `frontend/src/stores/chat.ts:3` and `workflow.ts:3` | +| `postcss` | 8.5.15 | High | Build-time transitive dependency; no user-controlled CSS processing found | +| `nanoid` | 3.3.12 | Moderate | Transitive build dependency; vulnerable custom size APIs are not called by project code | +| `esbuild` | 0.27.7 | Low | Development server issue affecting Windows | +| `vite` | 7.3.5 | Low | Inherits the esbuild advisory | +| `@vitejs/plugin-vue` | lockfile-resolved version | Low | Inherits the Vite advisory | + +`npm audit --omit=dev --json` still reported five advisories because the lockfile classifies much of the frontend graph as production dependencies. The production Python service serves compiled static assets and does not execute these npm packages at runtime. + +**Remediation** + +- Upgrade PostCSS/nanoid to patched versions through the Vite dependency graph. +- Upgrade `uuid` to a patched major version and run frontend tests/build checks. +- Keep Vite and plugin-vue aligned on supported versions. +- Add npm audit/SCA coverage to CI; the Black Duck workflow is configured for uv and its trigger does not match `package-lock.json`. + +### HF-09: MCP Client Certificate Temporary Files Are Not Deleted + +**Severity:** Low +**CWE:** CWE-459 (Incomplete Cleanup) +**Affected component:** MCP HTTP client + +**Evidence** + +- `agents/mcp/src/hyperforge_mcp/http.py:478-484` writes certificate material with `NamedTemporaryFile(delete=False)`. +- No subsequent unlink operation was found. + +**Impact** + +Private client certificates persist in the temporary directory and accumulate. Processes with equivalent privileges, diagnostic bundles, backups, or container snapshots may recover them. + +**Remediation** + +Delete the file immediately after `SSLContext.load_cert_chain()` in a `finally` block, enforce restrictive file permissions, and avoid disk-backed key material where the TLS library permits it. + +## Deployment-Dependent Risks + +### Trusted Header Authentication + +`hyperforge/src/hyperforge/api/authentication.py:33-94` accepts roles and users from `X-STF-*` and `X-NUCLIADB-*` headers without validating a signature or token. This is safe only if a trusted authorizer is the sole network peer and always strips client-supplied copies before injecting verified values. Direct API exposure would permit role spoofing. Enforce the trust boundary with network policy, mTLS, and proxy header sanitation; fail closed if the trusted authorizer is absent. + +### Restricted Python Isolation + +RestrictedPython is not an operating-system sandbox. Network verification is disabled by default in `agents/restricted/src/hyperforge_restricted/sandbox.py:25-42`, and debug execution can run in-process. No specific language escape was confirmed, but untrusted code should execute in a separate non-root container or microVM with no network, a read-only minimal filesystem, syscall restrictions, CPU/memory/process limits, and strict timeouts. + +### Frontend OAuth Link Scheme + +The frontend places an agent-provided OAuth URL directly into an anchor at `hyperforge/frontend/src/components/chat/ChatPanel.vue:72-77`, populated at `hyperforge/frontend/src/stores/chat.ts:184-190`. Vue escapes HTML, but URL schemes are not visibly allowlisted. Restrict links to `https:` and expected OAuth origins to prevent unsafe or deceptive schemes if a compromised integration supplies the URL. + +### Secret Classification Gaps + +Driver encryption covers declared `encrypted_fields`, but generic HTTP headers, external-agent headers, MCP certificates, and MCP stdio environment values are not consistently marked sensitive. Define a single secret-field policy, encrypt all persisted credentials, exclude them from API responses, and prevent them from entering logs or telemetry. + +## Positive Controls + +- SQL access reviewed uses SQLAlchemy expression construction and bound values; no concrete SQL injection was found. +- Runtime YAML loading uses `yaml.safe_load`; no pickle or equivalent unsafe deserialization was found. +- Vue templates reviewed use escaped interpolation rather than raw HTML rendering. +- Standalone JWT validation restricts algorithms and validates signature, expiration, optional not-before, issuer, audience, and scopes. +- The shared HTTP transport blocks IPv4 private and other non-global addresses, though HF-03 documents important gaps. +- Driver secret encryption uses Fernet and export encryption uses PBKDF2-derived keys. +- GitHub Actions references reviewed are pinned to full commit SHAs and checkout commonly disables credential persistence. +- Dedicated TruffleHog, Black Duck, Polaris, and zizmor workflows exist. +- `uv.lock` contains package hashes and `uv lock --check` passed. +- Common environment/credential files are ignored, and no tracked `.env`, private key, certificate, keystore, or Terraform state file was found. +- Renovate is configured in `.github/renovate.json`. + +## Scope and Method + +The review covered: + +- 22 Python workspace manifests and the root `uv.lock` (185 resolved packages). +- The Vue/Vite frontend manifest and lockfile. +- FastAPI HTTP, WebSocket, MCP, OAuth, standalone UI, authentication, database, encryption, and session code. +- HTTP, external, MCP, NucliaDB, Google, Perplexity, and restricted-code agent security boundaries. +- Dockerfile and 25 GitHub Actions workflows. +- Current-tree secret patterns and tracked sensitive filenames. +- Targeted searches for injection, process execution, deserialization, SSRF, XSS, file operations, and unbounded I/O. + +The worktree already contained uncommitted changes to: + +- `hyperforge/src/hyperforge/standalone/app.py` +- `hyperforge/src/hyperforge/standalone/settings.py` + +Those changes add configurable static-folder handling and were included in the reviewed state. They were not modified by this assessment. + +## Verification Results + +| Check | Result | +|---|---| +| `uv lock --check` | Passed; 185 packages resolved | +| `npm audit --json` | 6 advisories: 2 high, 1 moderate, 3 low | +| `npm audit --omit=dev --json` | 5 advisories: 2 high, 1 moderate, 2 low | +| `npm run build` | Passed with Vite 7.3.5 | +| `uv run pytest tests -q` in `agents/http` | 1 passed | +| `uv run pytest tests -q` in `agents/external` | 1 passed | +| `uv run pytest tests -q` in `agents/mcp` | 18 passed | +| `uv run pytest hyperforge/tests -q` | 159 passed; 48 setup errors because the Docker daemon was unavailable | +| Combined multi-package pytest invocation | Not valid due to duplicate `tests.conftest` module names; packages were rerun separately | +| Current-tree secret pattern scan | No confirmed production credential found | +| Tracked sensitive-file scan | No tracked matching files found | + +## Limitations + +- This was not a live penetration test and did not send exploit payloads to a deployed environment. +- Python advisory scanners (`pip-audit`, OSV Scanner), TruffleHog, Bandit, Semgrep, Syft, and Grype were not installed locally. Python vulnerabilities and full Git history secrets were therefore not independently queried. +- The Docker client was installed but the daemon was unavailable, so the image was not built or scanned and integration tests requiring PostgreSQL/Valkey containers could not start. +- Current-tree secret pattern matching cannot prove that all 304 repository commits are clean. +- Cloud, Kubernetes, reverse-proxy, firewall, mTLS, branch protection, GitHub environment, and secret-scope settings were not available for review. +- Dependency advisories describe affected versions; reachability judgments are based on static call-site review rather than runtime instrumentation. + +## Remediation Roadmap + +### Within 24 Hours + +- Restrict standalone and internal API network exposure. +- Fix HF-01 and HF-02 or disable the affected routes. +- Rotate exposed credentials where network exposure cannot be ruled out. + +### Within 7 Days + +- Correct SSRF handling across all configurable outbound requests. +- Add request/response limits for MCP and HTTP agents. +- Add `.dockerignore`, a least-privilege runtime user, and a minimal runtime image. +- Upgrade vulnerable frontend dependencies and add npm SCA to CI. + +### Within 30 Days + +- Run Python SCA, full-history secret scanning, SAST, and a built-image vulnerability scan in CI with blocking severity policies. +- Harden restricted-code execution with OS-level isolation. +- Add negative security tests for anonymous configuration access, internal route access, role-header spoofing, IPv6/private redirects, DNS rebinding, oversized payloads, and secret redaction. +- Review logging and telemetry for prompts, model context, external responses, OAuth data, and credentials. + +## Conclusion + +The project has several sound foundations, including parameterized database access, safe YAML parsing, encrypted driver fields, pinned GitHub Actions, and dedicated security workflows. However, HF-01 and HF-02 create direct credential-disclosure paths, and HF-03 can turn configuration control into internal network access. These issues should be treated as release blockers for any deployment reachable by untrusted clients. diff --git a/agents/external/src/hyperforge_external/agent.py b/agents/external/src/hyperforge_external/agent.py index 5f0d3453..4d9e8d3f 100644 --- a/agents/external/src/hyperforge_external/agent.py +++ b/agents/external/src/hyperforge_external/agent.py @@ -7,8 +7,7 @@ from hyperforge.manager import Manager from hyperforge.memory import QuestionMemory from hyperforge.trace import trace_agent -from hyperforge.utils import check_dns -from hyperforge.utils.http import safe_http_client +from hyperforge.utils.http import read_limited_response, safe_http_client from hyperforge_external.config import ExternalCallAgentConfig @@ -61,9 +60,8 @@ async def __call__( output_nuclia_tokens: float = 0.0 evaluation = None - async with safe_http_client() as client: - url = await check_dns(self.config.url) - + response_content = b"" + async with safe_http_client(timeout=self.config.timeout) as client: if self.config.call_schema: logger.debug( f"Calling external API with schema: {self.config.call_schema}, description: {self.config.description}" @@ -83,9 +81,9 @@ async def __call__( output_nuclia_tokens += output_tokens input_nuclia_tokens += input_tokens logger.debug(f"Json to do the call: {evaluation}") - resp = await client.request( + request = client.build_request( method=self.config.method.value, - url=url, + url=self.config.url, json=evaluation, headers=self.config.headers, ) @@ -93,9 +91,9 @@ async def __call__( logger.debug( f"Calling external API with generated context: {memory.contexts}" ) - resp = await client.request( + request = client.build_request( method=self.config.method.value, - url=url, + url=self.config.url, json=memory.contexts, headers=self.config.headers, ) @@ -109,26 +107,38 @@ async def __call__( evaluation = {} evaluation["answer"] = memory.final_answer evaluation["question"] = memory.original_question - resp = await client.request( + request = client.build_request( method=self.config.method.value, url=self.config.url, json=evaluation, headers=self.config.headers, ) + resp = await client.send(request, stream=True) + try: + response_content = await read_limited_response( + resp, self.config.max_response_bytes + ) + finally: + await resp.aclose() error_resp = None if resp is None: error_resp = "No response from external API" elif resp.status_code != 200: - error_resp = f"Error calling external API: {resp.status_code} - {resp.content.decode()}" + error_resp = f"Error calling external API: {resp.status_code}" if error_resp: logger.error(error_resp) raise Exception(error_resp) - logger.info(f"Response from external API: {resp.content.decode()}") + response_text = response_content.decode("utf-8", errors="replace") + logger.info( + "External API returned status %s and %s bytes", + resp.status_code, + len(response_content), + ) await memory.add_step( step_module="external", step_title=self.step_title(self.config.method.value), step_value=self.config.url, - step_reason=resp.content.decode(), + step_reason=response_text, timeit=time() - t0, step_agent_path=f"/postprocess/{self.config.id if self.config.id else 'default'}", input_nuclia_tokens=input_nuclia_tokens, diff --git a/agents/external/src/hyperforge_external/config.py b/agents/external/src/hyperforge_external/config.py index a013dea9..22c8018c 100644 --- a/agents/external/src/hyperforge_external/config.py +++ b/agents/external/src/hyperforge_external/config.py @@ -65,6 +65,13 @@ class ExternalCallAgentConfig(AgentConfig): title="URL to call", description="", ) + timeout: int = Field(default=30, ge=1, le=300) + max_response_bytes: int = Field( + default=1024 * 1024, + ge=1, + le=10 * 1024 * 1024, + description="Maximum accepted HTTP response size in bytes", + ) @field_validator("url") def validate_url(cls, v): diff --git a/agents/external/tests/test_external.py b/agents/external/tests/test_external.py index adbfdbf4..a44555ab 100644 --- a/agents/external/tests/test_external.py +++ b/agents/external/tests/test_external.py @@ -1,6 +1,7 @@ import os -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock +import httpx import pytest from hyperforge.engine import main as arag_main from hyperforge.minimal_fixtures import cassette_nua_key @@ -55,12 +56,12 @@ @pytest.mark.vcr @pytest.mark.asyncio async def test_external(mocker): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = b"oki doki" + request = httpx.Request("POST", "https://example.com/aaa") + mock_response = httpx.Response(200, content=b"oki doki", request=request) mock_client = AsyncMock() - mock_client.request = AsyncMock(return_value=mock_response) + mock_client.build_request.return_value = request + mock_client.send = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) @@ -87,4 +88,4 @@ async def test_external(mocker): "partner" in question_memory.steps[-2].reason or "Peugeot" in question_memory.steps[-2].reason ) - assert mock_client.request.called + assert mock_client.send.called diff --git a/agents/http/src/hyperforge_http/agent.py b/agents/http/src/hyperforge_http/agent.py index f8583fd4..d0d54fd1 100644 --- a/agents/http/src/hyperforge_http/agent.py +++ b/agents/http/src/hyperforge_http/agent.py @@ -7,8 +7,7 @@ from hyperforge.context.agent import ContextAgent from hyperforge.manager import Manager from hyperforge.memory import Chunk, Context, QuestionMemory -from hyperforge.utils import check_dns -from hyperforge.utils.http import safe_http_client +from hyperforge.utils.http import read_limited_response, safe_http_client from hyperforge_http.config import HTTPStaticAgentConfig @@ -33,24 +32,22 @@ async def _get_question_context( t0 = time() error = None - url = await check_dns(self.config.url) - async with safe_http_client() as client: if self.config.method == "GET": - response = await client.get( - url, + request = client.build_request( + "GET", + self.config.url, headers=self.config.headers, - timeout=self.config.timeout, params={self.config.question_query_param: question} if self.config.question_query_param else None, ) elif self.config.method == "POST": - response = await client.post( - url, + request = client.build_request( + "POST", + self.config.url, headers=self.config.headers, - timeout=self.config.timeout, params={self.config.question_query_param: question} if self.config.question_query_param else None, @@ -61,11 +58,19 @@ async def _get_question_context( else: raise ValueError(f"Unsupported HTTP method: {self.config.method}") + response = await client.send(request, stream=True) + try: + response_content = await read_limited_response( + response, self.config.max_response_bytes + ) + finally: + await response.aclose() + if response.status_code != 200: error = f"HTTP request failed with status code {response.status_code}" context_text = "" else: - context_text = response.content.decode("utf-8") + context_text = response_content.decode("utf-8", errors="replace") context = Context( agent_id=self.config.id if self.config.id else "http", @@ -88,7 +93,7 @@ async def _get_question_context( step_module=self.config.module, step_title=self.step_title("HTTP request"), step_agent_path=f"/context/{self.config.id if self.config.id else 'default'}", - step_value=f" HTTP {self.config.method} to {url}", + step_value=f" HTTP {self.config.method} to {self.config.url}", timeit=time() - t0, input_nuclia_tokens=0, output_nuclia_tokens=0, diff --git a/agents/http/src/hyperforge_http/config.py b/agents/http/src/hyperforge_http/config.py index 482acf40..803e6263 100644 --- a/agents/http/src/hyperforge_http/config.py +++ b/agents/http/src/hyperforge_http/config.py @@ -28,7 +28,13 @@ class HTTPStaticAgentConfig(ContextAgentConfig): description="Optional POST field id to include the question in the request body", ) timeout: int = Field( - default=10, description="Timeout for the HTTP request in seconds" + default=10, ge=1, le=300, description="Timeout for the HTTP request in seconds" + ) + max_response_bytes: int = Field( + default=1024 * 1024, + ge=1, + le=10 * 1024 * 1024, + description="Maximum accepted HTTP response size in bytes", ) @field_validator("url") diff --git a/agents/http/tests/test_http_static.py b/agents/http/tests/test_http_static.py index 78232159..882c310d 100644 --- a/agents/http/tests/test_http_static.py +++ b/agents/http/tests/test_http_static.py @@ -1,6 +1,7 @@ import os -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock +import httpx import pytest from hyperforge.engine import main as arag_main from hyperforge.interaction import AragAnswer @@ -46,12 +47,16 @@ async def test_http_static_get(mocker): answers = [] - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = b"Experts from the University of Nuclia have found that waking up relaxed can be achieved by following a consistent sleep schedule, creating a calming bedtime routine, and ensuring a comfortable sleep environment. Also, if your alarm is a an BetterStack call, it will help you clear your mind" + request = httpx.Request("GET", "https://example.com/context") + mock_response = httpx.Response( + 200, + content=b"Experts from the University of Nuclia have found that waking up relaxed can be achieved by following a consistent sleep schedule, creating a calming bedtime routine, and ensuring a comfortable sleep environment. Also, if your alarm is a an BetterStack call, it will help you clear your mind", + request=request, + ) mock_client = AsyncMock() - mock_client.get = AsyncMock(return_value=mock_response) + mock_client.build_request.return_value = request + mock_client.send = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) diff --git a/agents/mcp/src/hyperforge_mcp/agent.py b/agents/mcp/src/hyperforge_mcp/agent.py index e885e1bf..9e39d2e4 100644 --- a/agents/mcp/src/hyperforge_mcp/agent.py +++ b/agents/mcp/src/hyperforge_mcp/agent.py @@ -176,8 +176,15 @@ async def get_tool_selection_prompt( if final_prompt is None: # We will use LLM to choose the prompt - prompt_feedback_str = PROMPT_CHOOSE_TEMPLATE.render(prompts=self.prompts) - resp, input_tokens, output_tokens = await manager.execute_json( + prompt_feedback_str = PROMPT_CHOOSE_TEMPLATE.render( + prompts=self.prompts, task_description=question + ) + ( + resp, + input_tokens, + output_tokens, + reasoning, + ) = await manager.execute_json_reasoning( model=self.config.tool_choice_model, prompt=prompt_feedback_str, user_id="mcp_no_feedback", @@ -194,6 +201,7 @@ async def get_tool_selection_prompt( }, tracking=memory.get_tracking_info(), ) + breakpoint() prompt_id: str = resp["prompt_id"] await memory.add_step( step_module=self.config.module, diff --git a/agents/mcp/src/hyperforge_mcp/config_driver.py b/agents/mcp/src/hyperforge_mcp/config_driver.py index 6299fc3a..76014a86 100644 --- a/agents/mcp/src/hyperforge_mcp/config_driver.py +++ b/agents/mcp/src/hyperforge_mcp/config_driver.py @@ -18,7 +18,12 @@ def _redirect_uris_schema_default(schema: Dict[str, Any]) -> None: class MCPHTTPInnerConfig(EncryptedPayload): - encrypted_fields: ClassVar[list[str]] = ["client_secret"] + encrypted_fields: ClassVar[list[str]] = [ + "client_secret", + "headers", + "ca_certificate", + "crt_certificate", + ] uri: str timeout: float = 60 * 5 @@ -120,7 +125,7 @@ class MCPStdioServer(str, Enum): class MCPStdioInnerConfig(EncryptedPayload): - encrypted_fields: ClassVar[list[str]] = [] + encrypted_fields: ClassVar[list[str]] = ["env"] server: MCPStdioServer env: dict[str, str] | None = None diff --git a/agents/mcp/src/hyperforge_mcp/http.py b/agents/mcp/src/hyperforge_mcp/http.py index d17842a8..7578fd90 100644 --- a/agents/mcp/src/hyperforge_mcp/http.py +++ b/agents/mcp/src/hyperforge_mcp/http.py @@ -1,11 +1,13 @@ import asyncio import base64 import hashlib +import os import secrets import ssl import tempfile from collections.abc import Awaitable, Callable from functools import cache, partial +from pathlib import Path from typing import Any, Optional from urllib.parse import parse_qs, urlencode, urlparse from uuid import uuid4 @@ -478,10 +480,18 @@ def create_mcp_http_client( if certificate is not None: if len(certificate) > 32_000: raise ValueError("Certificate is too large") - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file: - cert_file.write(certificate) - - ssl_context.load_cert_chain(certfile=cert_file.name) + cert_path: str | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", delete=False, encoding="utf-8" + ) as cert_file: + cert_path = cert_file.name + cert_file.write(certificate) + os.chmod(cert_path, 0o600) + ssl_context.load_cert_chain(certfile=cert_path) + finally: + if cert_path is not None: + Path(cert_path).unlink(missing_ok=True) safe_transport = SafeTransport(verify=ssl_context) else: diff --git a/agents/mcp/src/hyperforge_mcp/multi.py b/agents/mcp/src/hyperforge_mcp/multi.py index 6fbc984a..388a6c96 100644 --- a/agents/mcp/src/hyperforge_mcp/multi.py +++ b/agents/mcp/src/hyperforge_mcp/multi.py @@ -102,7 +102,10 @@ async def multi_choose_tool( images: list[Image], ): t0 = time() - prompt_feedback = TOOLS_CHOOSE_TEMPLATE.render(tools=self.main_agent.tools) + prompt = " ".join([m.text for m in messages]) + prompt_feedback = TOOLS_CHOOSE_TEMPLATE.render( + tools=self.main_agent.tools, task_description=prompt + ) resp, input_tokens, output_tokens = await manager.execute_json( model=self.config.tool_choice_model, prompt=prompt_feedback, @@ -178,7 +181,7 @@ async def get_multi_tool_selection_prompt( if agent_obj is None: # We will use LLM to choose the prompt prompt_feedback_str = PROMPT_CHOOSE_TEMPLATE.render( - prompts=self.main_agent.prompts + prompts=self.main_agent.prompts, task_description=question ) resp, input_tokens, output_tokens = await manager.execute_json( model=self.config.tool_choice_model, diff --git a/agents/mcp/src/hyperforge_mcp/tools.py b/agents/mcp/src/hyperforge_mcp/tools.py index b3d525d8..d9a43869 100644 --- a/agents/mcp/src/hyperforge_mcp/tools.py +++ b/agents/mcp/src/hyperforge_mcp/tools.py @@ -7,6 +7,8 @@ {{prompt.description}} {% endfor -%} +Task: {{task_description}} + """ PROMPT_CHOOSE_TEMPLATE = PROMPT_ENVIRONMENT.from_string(PROMPT_CHOOSE) @@ -15,19 +17,10 @@ Choose proper toolset to solve the task. If none of the toolsets is suitable, do not choose any. Make sure to choose only one toolset. {% for tool in tools %} -{{tool.description}} +{{tool.description}} {% endfor -%} -""" - -PROMPT_CHOOSE_TEMPLATE = PROMPT_ENVIRONMENT.from_string(PROMPT_CHOOSE) - -PROMPT_CHOOSE = """ -Choose proper prompt to solve the task. If none of the prompts is suitable, do not choose any. Make sure to choose only one prompt. - -{% for prompt in prompts %} -{{prompt.description}} -{% endfor -%} +Task: {{task_description}} """ diff --git a/agents/mcp/tests/test_mcp.py b/agents/mcp/tests/test_mcp.py index dec7a342..2fc8f553 100644 --- a/agents/mcp/tests/test_mcp.py +++ b/agents/mcp/tests/test_mcp.py @@ -6,7 +6,6 @@ from hyperforge.engine import main as arag_main from hyperforge.interaction import AragAnswer from hyperforge.minimal_fixtures import cassette_nua_key -from hyperforge.utils.http import SafeTransport from hyperforge_mcp.agent import _tool_parameters @@ -86,7 +85,10 @@ async def test_tool_parameters_preserve_array_item_schema(): @pytest.fixture async def disable_safe_transport(): - with patch.object(SafeTransport, "is_private_address", return_value=False): + with patch( + "hyperforge.utils.http._resolve_public_addresses", + return_value=["127.0.0.1"], + ): yield diff --git a/agents/restricted/src/hyperforge_restricted/sandbox.py b/agents/restricted/src/hyperforge_restricted/sandbox.py index d9a32495..7b5366b1 100644 --- a/agents/restricted/src/hyperforge_restricted/sandbox.py +++ b/agents/restricted/src/hyperforge_restricted/sandbox.py @@ -30,7 +30,7 @@ class SandboxSettings(BaseSettings): # Verify that the sandbox is secure on startup or raise an exception - sandbox_verify: bool = False + sandbox_verify: bool = True # Socket for the sandbox server to listen to sandbox_socket: str | None = None diff --git a/agents/restricted/tests/test_sandbox.py b/agents/restricted/tests/test_sandbox.py index e7e6169a..8825468c 100644 --- a/agents/restricted/tests/test_sandbox.py +++ b/agents/restricted/tests/test_sandbox.py @@ -17,9 +17,15 @@ async def sandbox_conn(): with tempfile.TemporaryDirectory() as path: socket = f"{path}/sandbox.sock" - with patch( - "hyperforge_restricted.sandbox.settings.sandbox_socket", - socket, + with ( + patch( + "hyperforge_restricted.sandbox.settings.sandbox_socket", + socket, + ), + patch( + "hyperforge_restricted.sandbox.settings.sandbox_verify", + False, + ), ): task = asyncio.create_task(sandbox.run_sandbox_server()) diff --git a/hyperforge/frontend/package-lock.json b/hyperforge/frontend/package-lock.json index 06bade48..022c364f 100644 --- a/hyperforge/frontend/package-lock.json +++ b/hyperforge/frontend/package-lock.json @@ -13,15 +13,17 @@ "@vue-flow/core": "^1.41.8", "@vue-flow/minimap": "^1.5.0", "pinia": "^3.0.0", - "uuid": "^9.0.0", + "uuid": "11.1.1", "vue": "^3.4.0", "vue-router": "^5.0.0" }, "devDependencies": { "@types/uuid": "^9.0.7", - "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue": "6.0.8", + "esbuild": "0.28.1", + "postcss": "8.5.23", "typescript": "^5.3.3", - "vite": "^7.0.0", + "vite": "7.3.6", "vue-tsc": "^3.0.0" } }, @@ -135,9 +137,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -151,9 +153,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -167,9 +169,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -183,9 +185,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -199,9 +201,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -215,9 +217,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -231,9 +233,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -247,9 +249,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -263,9 +265,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -279,9 +281,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -295,9 +297,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -311,9 +313,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -327,9 +329,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -343,9 +345,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -359,9 +361,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -375,9 +377,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -391,9 +393,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -407,9 +409,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -423,9 +425,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -439,9 +441,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -455,9 +457,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -471,9 +473,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -487,9 +489,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -503,9 +505,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -519,9 +521,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -535,9 +537,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -595,6 +597,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", @@ -947,16 +956,19 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "version": "6.0.8", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "dev": true, "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, @@ -1472,9 +1484,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://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "devOptional": true, "hasInstallScript": true, "license": "MIT", @@ -1485,32 +1497,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/estree-walker": { @@ -1681,9 +1693,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1768,9 +1780,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.23", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -1787,7 +1799,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1978,26 +1990,26 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.1.1", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "7.3.6", + "resolved": "https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-progress_all-npm/npm/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "devOptional": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", diff --git a/hyperforge/frontend/package.json b/hyperforge/frontend/package.json index 61cd36f8..616b0690 100644 --- a/hyperforge/frontend/package.json +++ b/hyperforge/frontend/package.json @@ -14,15 +14,17 @@ "@vue-flow/core": "^1.41.8", "@vue-flow/minimap": "^1.5.0", "pinia": "^3.0.0", - "uuid": "^9.0.0", + "uuid": "11.1.1", "vue": "^3.4.0", "vue-router": "^5.0.0" }, "devDependencies": { "@types/uuid": "^9.0.7", - "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue": "6.0.8", + "esbuild": "0.28.1", + "postcss": "8.5.23", "typescript": "^5.3.3", - "vite": "^7.0.0", + "vite": "7.3.6", "vue-tsc": "^3.0.0" } -} \ No newline at end of file +} diff --git a/hyperforge/frontend/src/stores/chat.ts b/hyperforge/frontend/src/stores/chat.ts index a75e7fa5..c9cd3b70 100644 --- a/hyperforge/frontend/src/stores/chat.ts +++ b/hyperforge/frontend/src/stores/chat.ts @@ -5,6 +5,7 @@ import { connectChat } from '@/api/chatWs' import type { ChatSocket } from '@/api/chatWs' import { AnswerOperation } from '@/types/arag' import type { AragAnswer, AragContext, AragStep, Feedback } from '@/types/arag' +import { safeOAuthUrl } from '@/utils/oauthUrl' export interface ChatMessage { id: string @@ -186,7 +187,7 @@ function _applyChunk(msg: ChatMessage, chunk: AragAnswer) { // from a WS message handler is reliably blocked by browsers since // there's no preceding user gesture. The agent resumes on its own // once OAuth completes, so we don't pause streaming here. - msg.pendingOAuthUrl = chunk.oauth.oauth_url + msg.pendingOAuthUrl = safeOAuthUrl(chunk.oauth.oauth_url) } } if (chunk.operation === AnswerOperation.ERROR && chunk.exception) { diff --git a/hyperforge/frontend/src/utils/oauthUrl.ts b/hyperforge/frontend/src/utils/oauthUrl.ts new file mode 100644 index 00000000..3710789a --- /dev/null +++ b/hyperforge/frontend/src/utils/oauthUrl.ts @@ -0,0 +1,11 @@ +export function safeOAuthUrl(value: string): string | undefined { + try { + const url = new URL(value) + if (url.protocol !== 'https:' || !url.hostname || url.username || url.password) { + return undefined + } + return url.href + } catch { + return undefined + } +} diff --git a/hyperforge/pyproject.toml b/hyperforge/pyproject.toml index b366cbd0..d5ab7427 100644 --- a/hyperforge/pyproject.toml +++ b/hyperforge/pyproject.toml @@ -28,7 +28,8 @@ dependencies = [ "prometheus_client", "mrflagly>=0.2.11", "aiodns", - "httpx", + "httpx>=0.28.1,<0.29", + "httpcore>=1.0.9,<1.1", "jinja2", "databases[asyncpg]", "sqlalchemy", diff --git a/hyperforge/src/hyperforge/api/app.py b/hyperforge/src/hyperforge/api/app.py index 8fcddf5c..9ac2f7a3 100644 --- a/hyperforge/src/hyperforge/api/app.py +++ b/hyperforge/src/hyperforge/api/app.py @@ -16,7 +16,7 @@ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.responses import PlainTextResponse -from hyperforge.api import SERVICE_NAME, internal, logger, v1 +from hyperforge.api import SERVICE_NAME, logger, v1 from hyperforge.api.authentication import RaoAuthenticationBackend from hyperforge.api.logging import set_sentry from hyperforge.api.settings import Settings @@ -74,7 +74,6 @@ async def lifespan(app: "HTTPApplication"): self.settings = settings self.data_manager_settings = data_manager_settings self._agents_cfg = {} - self.include_router(internal.router) self.include_router(v1.router) self.include_router(router) self.add_middleware( diff --git a/hyperforge/src/hyperforge/api/settings.py b/hyperforge/src/hyperforge/api/settings.py index 15b9ad1a..2ff2d3ca 100644 --- a/hyperforge/src/hyperforge/api/settings.py +++ b/hyperforge/src/hyperforge/api/settings.py @@ -38,3 +38,5 @@ class Settings(BaseSettings): hydra_scopes_supported: list[str] = ["offline_access", "openid"] mcp_force_https_metadata: bool = True auth_success_logo_url: Optional[str] = None + mcp_max_request_bytes: int = 1024 * 1024 + mcp_max_response_bytes: int = 4 * 1024 * 1024 diff --git a/hyperforge/src/hyperforge/api/v1/mcp_interaction.py b/hyperforge/src/hyperforge/api/v1/mcp_interaction.py index 521713ba..8b427aab 100644 --- a/hyperforge/src/hyperforge/api/v1/mcp_interaction.py +++ b/hyperforge/src/hyperforge/api/v1/mcp_interaction.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Iterable, Sequence import anyio -from fastapi import Header +from fastapi import Header, HTTPException from mcp.server.fastmcp.exceptions import ResourceError from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.lowlevel.server import Server as MCPServer @@ -310,6 +310,23 @@ async def interaction_mcp_handler( x_stf_account_type: str = Header(..., include_in_schema=False), ): app: HTTPApplication = request.app + runtime_settings = getattr(app, "_standalone_settings", app.settings) + max_request_bytes = runtime_settings.mcp_max_request_bytes + max_response_bytes = runtime_settings.mcp_max_response_bytes + content_length = request.headers.get("content-length") + if content_length is not None: + try: + if int(content_length) > max_request_bytes: + raise HTTPException(status_code=413, detail="MCP request is too large") + except ValueError: + raise HTTPException(status_code=400, detail="Invalid Content-Length") from None + + body_bytes = bytearray() + async for chunk in request.stream(): + if len(body_bytes) + len(chunk) > max_request_bytes: + raise HTTPException(status_code=413, detail="MCP request is too large") + body_bytes.extend(chunk) + agent_manager: AgentManager = request.app.agent_manager request._headers._list.append((MCP_SESSION_ID_HEADER.encode(), session.encode())) @@ -380,16 +397,26 @@ async def run_stateless_server( response_status = 200 response_headers: dict[str, str] = {} body_chunks: list[bytes] = [] + response_bytes = 0 + response_too_large = False async def intercepting_send(message: MutableMapping[str, Any]) -> None: - nonlocal response_status + nonlocal response_status, response_bytes, response_too_large if message["type"] == "http.response.start": response_status = message["status"] response_headers.update( {k.decode(): v.decode() for k, v in message.get("headers", [])} ) elif message["type"] == "http.response.body": - body_chunks.append(message.get("body", b"")) + chunk = message.get("body", b"") + if not chunk or response_too_large: + return + response_bytes += len(chunk) + if response_bytes > max_response_bytes: + response_too_large = True + body_chunks.clear() + return + body_chunks.append(chunk) # Pre-read the body before passing to the MCP transport. # Fix for FastAPI/Starlette 1.x @@ -397,14 +424,17 @@ async def intercepting_send(message: MutableMapping[str, Any]) -> None: # TODO: consider rewriting this handler to use the official StreamableHTTPSessionManager # This does not happen in arag due to older versions of FastAPI/Starlette. - body_bytes = await request.body() body_sent = False async def patched_receive(): nonlocal body_sent if not body_sent: body_sent = True - return {"type": "http.request", "body": body_bytes, "more_body": False} + return { + "type": "http.request", + "body": bytes(body_bytes), + "more_body": False, + } while True: msg = await request._receive() if msg["type"] == "http.disconnect": @@ -422,6 +452,9 @@ async def patched_receive(): # Terminate the transport after the request is handled await http_transport.terminate() + if response_too_large: + return Response(content="MCP response is too large", status_code=502) + return Response( content=b"".join(body_chunks), status_code=response_status, diff --git a/hyperforge/src/hyperforge/interaction.py b/hyperforge/src/hyperforge/interaction.py index e03bf6cd..181ad7e8 100644 --- a/hyperforge/src/hyperforge/interaction.py +++ b/hyperforge/src/hyperforge/interaction.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Any, Dict, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from hyperforge.models import ( Answer, @@ -11,6 +11,7 @@ Step, Visualization, ) +from hyperforge.utils.http import validate_public_http_url # Models used between client and API for itneraction requests/responses/streams @@ -66,6 +67,11 @@ class Provider(Enum): class OAuthAuthenticateURL(BaseModel): oauth_url: str + @field_validator("oauth_url") + @classmethod + def validate_oauth_url(cls, value: str) -> str: + return validate_public_http_url(value, https_only=True) + class OAuthFeedbackReturnSchema(BaseModel): existing_credentials: Optional[Dict[str, Dict[str, str]]] = None diff --git a/hyperforge/src/hyperforge/manager.py b/hyperforge/src/hyperforge/manager.py index 9ad0f716..5ae73b3e 100644 --- a/hyperforge/src/hyperforge/manager.py +++ b/hyperforge/src/hyperforge/manager.py @@ -515,6 +515,57 @@ async def execute_json( output_tokens, ) + async def execute_json_reasoning( + self, + prompt: str, + user_id: str, + schema: Dict[str, Any], + model: ModelParam, + images: Dict[str, Image] = {}, + system: Optional[str] = None, + max_tokens: int = 8192, + tracking: TrackingInfo | None = None, + ) -> Tuple[Dict[str, Any], float, float, str | None]: + breakpoint() + try: + resp = await self.nua.generate( + body=ChatModel( + user_id=user_id, + question="", + user_prompt=UserPrompt(prompt=prompt), + generative_model=_resolve_model_id(model), + reasoning=build_reasoning(model), + format_prompt=False, + query_context_images=images, + json_schema=schema, + system=system, + max_tokens=max_tokens, + citations=False, + ), + extra_headers=self._build_extra_headers(tracking), + ) + + except ValidationError as e: + convert_errors(e) + raise + + if resp.object is None: + raise Exception("No object") + + if resp.consumption is None or resp.consumption.normalized_tokens is None: + input_tokens = 0.0 + output_tokens = 0.0 + else: + input_tokens = resp.consumption.normalized_tokens.input + output_tokens = resp.consumption.normalized_tokens.output + breakpoint() + return ( + resp.object, + input_tokens, + output_tokens, + resp.reasoning, + ) + async def execute_json_citation( self, question: str, diff --git a/hyperforge/src/hyperforge/standalone/app.py b/hyperforge/src/hyperforge/standalone/app.py index e1954b62..a8c3e7fb 100644 --- a/hyperforge/src/hyperforge/standalone/app.py +++ b/hyperforge/src/hyperforge/standalone/app.py @@ -6,6 +6,8 @@ NucliaDB required. """ +import base64 +import secrets from contextlib import asynccontextmanager from pathlib import Path from typing import Any, Tuple @@ -110,13 +112,20 @@ async def health_alive(): class StandaloneAuthBackend(AuthenticationBackend): """Open standalone auth, with optional OAuth protection for MCP endpoints.""" - def __init__(self, agents_cfg: dict[str, Any]) -> None: + def __init__( + self, agents_cfg: dict[str, Any], username: str, password: str | None + ) -> None: self._agents_cfg = agents_cfg + self._ui_username = username + self._ui_password = password self._jwks_cache = JWKSCache() async def authenticate( self, conn: HTTPConnection ) -> tuple[AuthCredentials, BaseUser] | None: + if conn.url.path.startswith("/api/v1/ui/"): + self._authenticate_ui(conn) + agent_id = self._mcp_agent_id(conn) if agent_id is not None: auth_config = get_enabled_mcp_auth(self._agents_cfg, agent_id) @@ -148,6 +157,28 @@ async def authenticate( ] return _ALL_ROLES, User(username="standalone") + def _authenticate_ui(self, conn: HTTPConnection) -> None: + if self._ui_password is None: + return + + authorization = conn.headers.get("authorization") + if authorization is None: + raise AuthenticationError("Missing administrator credentials") + scheme, _, encoded = authorization.partition(" ") + if scheme.lower() != "basic" or not encoded: + raise AuthenticationError("Invalid administrator credentials") + try: + username, separator, password = base64.b64decode( + encoded, validate=True + ).decode("utf-8").partition(":") + except (ValueError, UnicodeDecodeError): + raise AuthenticationError("Invalid administrator credentials") from None + if not separator or not ( + secrets.compare_digest(username, self._ui_username) + and secrets.compare_digest(password, self._ui_password) + ): + raise AuthenticationError("Invalid administrator credentials") + def _mcp_agent_id(self, conn: HTTPConnection) -> str | None: parts = conn.url.path.strip("/").split("/") if ( @@ -163,6 +194,12 @@ def _mcp_agent_id(self, conn: HTTPConnection) -> str | None: def standalone_auth_error( conn: HTTPConnection, exc: AuthenticationError ) -> PlainTextResponse: + if conn.url.path.startswith("/api/v1/ui/"): + return PlainTextResponse( + str(exc) or "Unauthorized", + status_code=401, + headers={"WWW-Authenticate": 'Basic realm="Hyperforge administration"'}, + ) authenticate = "Bearer" metadata_url = _mcp_resource_metadata_url(conn) if metadata_url is not None: @@ -239,13 +276,14 @@ async def lifespan(app: "StandaloneApplication"): self.include_router(v1.mcp_interaction.router) self.include_router(v1_oauth.router) self.include_router(router) - self.include_router(ui_router) + if settings.ui_admin_password is not None: + self.include_router(ui_router) # Serve the built frontend SPA if the dist directory exists. # In development the Vite dev server runs separately (proxied to :8080). # app.py lives at: arag/src/hyperforge.standalone/app.py # three .parent steps → arag/ - _frontend_dist = Path(__file__).parent / "static" + _frontend_dist = Path(settings.static_folder).resolve() if _frontend_dist.is_dir(): self.mount( "/", @@ -255,7 +293,13 @@ async def lifespan(app: "StandaloneApplication"): self.add_middleware( AuthenticationMiddleware, - backend=StandaloneAuthBackend(agents_cfg), + backend=StandaloneAuthBackend( + agents_cfg, + settings.ui_admin_username, + settings.ui_admin_password.get_secret_value() + if settings.ui_admin_password is not None + else None, + ), on_error=standalone_auth_error, ) self.add_middleware( diff --git a/hyperforge/src/hyperforge/standalone/oauth.py b/hyperforge/src/hyperforge/standalone/oauth.py index 34500c67..d8c5b37b 100644 --- a/hyperforge/src/hyperforge/standalone/oauth.py +++ b/hyperforge/src/hyperforge/standalone/oauth.py @@ -4,12 +4,16 @@ from dataclasses import dataclass, field from typing import Any -import httpx from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.hashes import SHA256, SHA384, SHA512 from starlette.authentication import AuthenticationError from hyperforge.standalone.config import StandAloneAgentConfig, StandaloneMCPAuthConfig +from hyperforge.utils.http import ( + read_limited_response, + safe_http_client, + validate_public_http_url, +) _HASHES = { "RS256": SHA256(), @@ -33,6 +37,7 @@ class JWKSCache: _values: dict[str, tuple[float, dict[str, Any]]] = field(default_factory=dict) async def get(self, url: str) -> dict[str, Any]: + validate_public_http_url(url, https_only=True) now = time.time() cached = self._values.get(url) if cached is not None: @@ -40,10 +45,15 @@ async def get(self, url: str) -> dict[str, Any]: if expires_at > now: return jwks - async with httpx.AsyncClient(timeout=10) as client: - response = await client.get(url) - response.raise_for_status() - jwks = response.json() + async with safe_http_client(timeout=10) as client: + request = client.build_request("GET", url) + response = await client.send(request, stream=True, follow_redirects=True) + try: + response.raise_for_status() + content = await read_limited_response(response, 1024 * 1024) + finally: + await response.aclose() + jwks = json.loads(content) self._values[url] = (now + self.ttl_seconds, jwks) return jwks diff --git a/hyperforge/src/hyperforge/standalone/settings.py b/hyperforge/src/hyperforge/standalone/settings.py index 583e95c7..6e9c651a 100644 --- a/hyperforge/src/hyperforge/standalone/settings.py +++ b/hyperforge/src/hyperforge/standalone/settings.py @@ -13,11 +13,15 @@ from pathlib import Path from typing import Optional -from pydantic import AliasChoices, Field -from pydantic_settings import BaseSettings +from pydantic import AliasChoices, Field, SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict class StandaloneSettings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", extra="ignore" + ) + # ------------------------------------------------------------------ # Agent config file # ------------------------------------------------------------------ @@ -31,7 +35,7 @@ class StandaloneSettings(BaseSettings): # HTTP server # ------------------------------------------------------------------ - host: str = Field(default="0.0.0.0", description="Listen host.") + host: str = Field(default="127.0.0.1", description="Listen host.") port: int = Field(default=8080, description="Listen port.") log_level: str = Field(default="INFO", description="Log level (uvicorn + app).") debug: bool = Field(default=False, description="Enable debug mode.") @@ -116,15 +120,25 @@ class StandaloneSettings(BaseSettings): ) in_memory_cache_size: int = 3000 + mcp_max_request_bytes: int = Field(default=1024 * 1024, ge=1) + mcp_max_response_bytes: int = Field(default=4 * 1024 * 1024, ge=1) cors_allow_origin: list[str] = Field( - default_factory=lambda: ["*"], + default_factory=list, description=( "List of allowed origins for CORS. Defaults to ['*'] which allows all origins. " "In production, it's recommended to set this to the specific origins that should be allowed." ), ) + ui_admin_username: str = Field( + default="admin", description="Username for the standalone configuration UI." + ) + ui_admin_password: SecretStr | None = Field( + default=None, + description="Password that enables and protects the standalone configuration UI.", + ) + load_modules: list[str] = [] # ------------------------------------------------------------------ # Pluggable module classes @@ -146,3 +160,8 @@ class StandaloneSettings(BaseSettings): default="hyperforge.standalone.app.StandaloneApplication", description="Dotted-name of the StandaloneApplication class to use.", ) + + static_folder: str = Field( + default=str(Path(__file__).parent.parent / "static"), + description="Path to the static folder for the frontend.", + ) diff --git a/hyperforge/src/hyperforge/utils/__init__.py b/hyperforge/src/hyperforge/utils/__init__.py index 58a5eb9f..715f0f00 100644 --- a/hyperforge/src/hyperforge/utils/__init__.py +++ b/hyperforge/src/hyperforge/utils/__init__.py @@ -1,11 +1,8 @@ -import ipaddress -import socket import urllib.parse from collections.abc import Iterable from enum import Enum from typing import Any, Dict, Optional, Tuple -import aiodns from nuclia_models.predict.generative_responses import GenerativeFullResponse @@ -22,19 +19,13 @@ def iterate_tools_resp( def sync_check_dns(url: str) -> Optional[str]: parsed_url = urllib.parse.urlparse(url) - error = None - try: - if parsed_url.hostname is None: - hostname = parsed_url.path - else: - hostname = parsed_url.hostname - addr = socket.gethostbyname_ex(hostname) - for addres in addr[2]: - if ipaddress.ip_address(addres).is_private: - error = "Its a private address" - except aiodns.error.DNSError: - error = "Could not find this URL" - return error + if parsed_url.scheme.lower() not in {"http", "https"}: + return "URL must use HTTP or HTTPS" + if parsed_url.hostname is None: + return "URL must include a hostname" + if parsed_url.username is not None or parsed_url.password is not None: + return "Credentials are not allowed in URLs" + return None def sync_dns_validation(url: str) -> str: @@ -45,24 +36,7 @@ def sync_dns_validation(url: str) -> str: async def check_dns(url: str) -> str: - parsed_url = urllib.parse.urlparse(url) - resolver = aiodns.DNSResolver() - error = None - try: - if parsed_url.hostname is None: - hostname = parsed_url.path - else: - hostname = parsed_url.hostname - addr = await resolver.gethostbyname(hostname, socket.AF_INET) - for addres in addr.addresses: - if ipaddress.ip_address(addres).is_private: - error = "Its a private address" - except aiodns.error.DNSError: - error = "Could not find this URL" - - if error is not None: - raise ValueError(error) - return url + return sync_dns_validation(url) class WidgetType(str, Enum): diff --git a/hyperforge/src/hyperforge/utils/http.py b/hyperforge/src/hyperforge/utils/http.py index 5735e014..bdf4093c 100644 --- a/hyperforge/src/hyperforge/utils/http.py +++ b/hyperforge/src/hyperforge/utils/http.py @@ -1,48 +1,135 @@ import ipaddress import socket +from collections.abc import AsyncIterator, Iterable +from typing import Any -import aiodns -from httpx import AsyncClient, AsyncHTTPTransport +import anyio +import httpcore +from httpcore._backends.base import SOCKET_OPTION, AsyncNetworkStream +from httpx import AsyncClient, AsyncHTTPTransport, Request, Response class PrivateUrlError(Exception): pass +class ResponseTooLargeError(Exception): + pass + + +def validate_public_http_url(url: str, *, https_only: bool = False) -> str: + from urllib.parse import urlsplit + + parsed = urlsplit(url) + allowed_schemes = {"https"} if https_only else {"http", "https"} + if parsed.scheme.lower() not in allowed_schemes or parsed.hostname is None: + raise ValueError("URL must use an allowed HTTP scheme and include a hostname") + if parsed.username is not None or parsed.password is not None: + raise ValueError("Credentials are not allowed in URLs") + return url + + +async def _resolve_public_addresses(host: str, port: int) -> list[str]: + try: + addresses = await anyio.getaddrinfo( + host, + port, + family=socket.AF_UNSPEC, + type=socket.SOCK_STREAM, + ) + except OSError as exc: + raise PrivateUrlError("Could not resolve hostname") from exc + + resolved = list(dict.fromkeys(address[4][0] for address in addresses)) + if not resolved or any( + not ipaddress.ip_address(address.split("%", 1)[0]).is_global + for address in resolved + ): + raise PrivateUrlError("Cannot access non-public network resources") + return resolved + + +class SafeNetworkBackend(httpcore.AsyncNetworkBackend): + def __init__(self) -> None: + self._backend = httpcore.AsyncConnectionPool()._network_backend + + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + addresses = await _resolve_public_addresses(host, port) + last_error: Exception | None = None + for address in addresses: + try: + return await self._backend.connect_tcp( + address, + port, + timeout=timeout, + local_address=local_address, + socket_options=socket_options, + ) + except Exception as exc: + last_error = exc + assert last_error is not None + raise last_error + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + raise PrivateUrlError("Unix sockets are not allowed") + + async def sleep(self, seconds: float) -> None: + await self._backend.sleep(seconds) + + class SafeTransport(AsyncHTTPTransport): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) + self._pool._network_backend = SafeNetworkBackend() + + async def handle_async_request(self, request: Request) -> Response: + validate_public_http_url(str(request.url)) + return await super().handle_async_request(request) + - @staticmethod - async def is_private_address(hostname: str) -> bool: - resolver = aiodns.DNSResolver() - addr = await resolver.gethostbyname(hostname, socket.AF_INET) - for address in addr.addresses: - if ipaddress.ip_address(address).is_private: - return True - if ( - not ipaddress.ip_address(address).is_private - and not ipaddress.ip_address(address).is_global - ): - # Matches shared address space (100.64.0.0/10 range) that should be non-routeable and - # not be used for public internet. Let's consider it internal - return True - return False - - async def handle_async_request(self, request): - url = request.url +async def read_limited_response(response: Response, max_bytes: int) -> bytes: + content_length = response.headers.get("content-length") + if content_length is not None: try: - hostname = url.host if url.host is not None else url.path - if await self.is_private_address(hostname): - raise PrivateUrlError("Cannot access private network resources") - except aiodns.error.DNSError: - raise PrivateUrlError("Could not lookup hostname") + if int(content_length) > max_bytes: + raise ResponseTooLargeError("HTTP response exceeds configured limit") + except ValueError: + pass - return await super().handle_async_request(request) + content = bytearray() + async for chunk in response.aiter_bytes(): + if len(content) + len(chunk) > max_bytes: + raise ResponseTooLargeError("HTTP response exceeds configured limit") + content.extend(chunk) + return bytes(content) + + +async def iter_limited_bytes( + chunks: AsyncIterator[bytes], max_bytes: int +) -> AsyncIterator[bytes]: + total = 0 + async for chunk in chunks: + total += len(chunk) + if total > max_bytes: + raise ResponseTooLargeError("HTTP response exceeds configured limit") + yield chunk -def safe_http_client(timeout=30, transport_verify: bool = True): +def safe_http_client(timeout: float = 30, transport_verify: bool = True) -> AsyncClient: return AsyncClient( timeout=timeout, transport=SafeTransport(verify=transport_verify), + trust_env=False, ) diff --git a/hyperforge/tests/standalone/test_standalone.py b/hyperforge/tests/standalone/test_standalone.py index f758e99b..7852fea8 100644 --- a/hyperforge/tests/standalone/test_standalone.py +++ b/hyperforge/tests/standalone/test_standalone.py @@ -1,5 +1,6 @@ import json import socket +from base64 import b64encode from pathlib import Path from typing import Any, Optional from unittest.mock import patch @@ -12,6 +13,7 @@ from mcp import ClientSession from mcp.client.streamable_http import streamable_http_client from mcp.types import TextContent +from pydantic import SecretStr from websockets.asyncio.client import connect from hyperforge.api.models import ( @@ -210,6 +212,45 @@ async def test_standalone_http_post(standalone_client: AsyncClient): assert "42" in answer_msgs[-1].answer +async def test_standalone_ui_is_disabled_without_password( + standalone_client: AsyncClient, +): + response = await standalone_client.get("/api/v1/ui/config") + assert response.status_code == 404 + + +async def test_standalone_ui_requires_admin_credentials(local_agents_config): + settings = STANDALONE_SETTINGS.model_copy( + update={"ui_admin_password": SecretStr("secret-password")} + ) + app = StandaloneApplication(local_agents_config, settings) + with patch("hyperforge.server.session.get_state", return_value=_mock_state): + async with ( + app.router.lifespan_context(app), + AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as client, + ): + response = await client.get("/api/v1/ui/config") + assert response.status_code == 401 + assert response.headers["www-authenticate"].startswith("Basic ") + + response = await client.get( + "/api/v1/ui/config", + headers={ + "Authorization": "Basic " + + b64encode(b"admin:secret-password").decode() + }, + ) + assert response.status_code == 200 + + +async def test_standalone_security_defaults(): + settings = StandaloneSettings() + assert settings.host == "127.0.0.1" + assert settings.cors_allow_origin == [] + + async def test_standalone_http_post_unknown_agent(standalone_client: AsyncClient): """Asking a non-existent agent_id should return an error operation, not a 500.""" async with standalone_client.stream( diff --git a/hyperforge/tests/unit/arag/test_internal_routes.py b/hyperforge/tests/unit/arag/test_internal_routes.py new file mode 100644 index 00000000..d24f82bd --- /dev/null +++ b/hyperforge/tests/unit/arag/test_internal_routes.py @@ -0,0 +1,15 @@ +from hyperforge.api.app import HTTPApplication +from hyperforge.api.settings import Settings +from hyperforge.db.settings import DataManagerSettings + + +def test_internal_inspection_route_is_not_mounted(): + app = HTTPApplication( + Settings(), + DataManagerSettings(postgresql_dsn="postgresql://unused"), + ) + + assert all( + getattr(route, "path", None) != "/api/internal/v1/agent/{kbid}" + for route in app.routes + ) diff --git a/hyperforge/tests/unit/test_safe_http.py b/hyperforge/tests/unit/test_safe_http.py new file mode 100644 index 00000000..8e573a9c --- /dev/null +++ b/hyperforge/tests/unit/test_safe_http.py @@ -0,0 +1,52 @@ +import socket +from unittest.mock import AsyncMock, patch + +import pytest + +from hyperforge.utils.http import ( + PrivateUrlError, + ResponseTooLargeError, + _resolve_public_addresses, + read_limited_response, + validate_public_http_url, +) + + +@pytest.mark.parametrize( + "url", + ["file:///etc/passwd", "http://user:pass@example.com", "/relative"], +) +def test_validate_public_http_url_rejects_unsafe_urls(url: str): + with pytest.raises(ValueError): + validate_public_http_url(url) + + +def test_validate_public_http_url_requires_https_when_requested(): + with pytest.raises(ValueError): + validate_public_http_url("http://example.com", https_only=True) + assert ( + validate_public_http_url("https://example.com", https_only=True) + == "https://example.com" + ) + + +@pytest.mark.asyncio +async def test_resolver_rejects_any_non_public_address(): + answers = [ + (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 443)), + (socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("::1", 443, 0, 0)), + ] + with ( + patch("anyio.getaddrinfo", AsyncMock(return_value=answers)), + pytest.raises(PrivateUrlError), + ): + await _resolve_public_addresses("example.com", 443) + + +@pytest.mark.asyncio +async def test_read_limited_response_rejects_large_body(): + import httpx + + response = httpx.Response(200, content=b"12345") + with pytest.raises(ResponseTooLargeError): + await read_limited_response(response, 4) diff --git a/uv.lock b/uv.lock index 56118033..80f1cab8 100644 --- a/uv.lock +++ b/uv.lock @@ -941,6 +941,7 @@ dependencies = [ { name = "cryptography" }, { name = "databases", extra = ["asyncpg"] }, { name = "fastapi" }, + { name = "httpcore" }, { name = "httpx" }, { name = "hyperforge-restricted" }, { name = "jinja2" }, @@ -990,7 +991,8 @@ requires-dist = [ { name = "cryptography" }, { name = "databases", extras = ["asyncpg"] }, { name = "fastapi" }, - { name = "httpx" }, + { name = "httpcore", specifier = ">=1.0.9,<1.1" }, + { name = "httpx", specifier = ">=0.28.1,<0.29" }, { name = "hyperforge-restricted", editable = "agents/restricted" }, { name = "jinja2" }, { name = "lru-dict" }, From cc0404c1881cce386161bcf59d8c489bce6086b9 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Fri, 28 Aug 2026 11:10:13 +0200 Subject: [PATCH 2/9] Breakpoints --- SECURITY_SCAN_REPORT.md | 369 ------------------------- agents/mcp/src/hyperforge_mcp/agent.py | 1 - hyperforge/src/hyperforge/manager.py | 2 - 3 files changed, 372 deletions(-) delete mode 100644 SECURITY_SCAN_REPORT.md diff --git a/SECURITY_SCAN_REPORT.md b/SECURITY_SCAN_REPORT.md deleted file mode 100644 index 77e15362..00000000 --- a/SECURITY_SCAN_REPORT.md +++ /dev/null @@ -1,369 +0,0 @@ -# Security Assessment Report - -**Project:** Hyperforge -**Assessment date:** 2026-08-26 -**Revision reviewed:** `4c0d323606216447f795b7a463657e6d1dcf17e0` (`main`) plus the existing uncommitted changes listed under Scope -**Assessment type:** Static source, configuration, dependency, secret-pattern, and targeted test review - -> **Remediation update (2026-08-26):** All confirmed findings in this report have been addressed in the working tree. HF-01 through HF-09 are fixed pending deployment and release. The validation section below records the original scan; post-remediation checks include a clean npm audit, successful frontend build, lint/type checks, and passing targeted security and agent tests. Docker-dependent integration and image scans remain blocked by the unavailable local Docker daemon. - -## Executive Summary - -Hyperforge has two credential-disclosure paths that should be addressed before either affected service is reachable from an untrusted network. Standalone mode deliberately grants every request all roles while exposing endpoints that return and replace the complete agent configuration. The full API also mounts an unauthenticated internal inspection endpoint that returns decrypted driver configurations for a caller-selected account and agent. - -The outbound HTTP safeguard is a useful control, but it validates only IPv4 and performs DNS validation separately from connection establishment. This leaves IPv6 and DNS-rebinding SSRF paths, particularly serious when combined with standalone configuration access. Several request and response paths also buffer data without size limits, creating denial-of-service risk. - -The frontend lockfile contains six npm advisories (two high, one moderate, and three low). The two high advisories are not directly exploitable through the application behavior observed: Hyperforge uses only UUID v4, and PostCSS runs as a build dependency without processing user-submitted CSS. They should still be upgraded to restore a clean supply chain. - -No obvious committed production credential, private key, SQL injection, shell injection, unsafe YAML deserialization, or direct template XSS was confirmed in the current tracked tree. - -### Finding Summary - -| ID | Severity | Finding | Status | -|---|---|---|---| -| HF-01 | Critical | Standalone configuration and secrets are exposed without authentication | Remediated | -| HF-02 | High | Internal inspection endpoint returns decrypted credentials without authorization | Remediated | -| HF-03 | High | SSRF protection is bypassable through IPv6 or DNS rebinding | Remediated | -| HF-04 | High | Docker build can copy local secrets and Git metadata into the runtime image | Remediated | -| HF-05 | Medium | MCP request and response buffering permits memory exhaustion | Remediated | -| HF-06 | Medium | HTTP agents accept unbounded remote response bodies | Remediated | -| HF-07 | Medium | Runtime container executes as root | Remediated | -| HF-08 | Medium | Locked npm dependencies have known vulnerabilities | Remediated | -| HF-09 | Low | MCP client certificate temporary files are not deleted | Remediated | - -## Immediate Actions - -1. Prevent network access to standalone mode and `/api/internal/*` until HF-01 and HF-02 are fixed. -2. Rotate credentials if either endpoint has been exposed to an untrusted network; access logs alone cannot prove secrets were not read. -3. Require authenticated administrative authorization for standalone configuration access and redact every secret-bearing field. -4. Remove or strongly authenticate the internal inspection endpoint and never serialize decrypted driver secrets. -5. Treat configurable outbound URLs as SSRF-capable until DNS and connection validation are made atomic for IPv4 and IPv6. - -## Detailed Findings - -### HF-01: Standalone Configuration and Secrets Exposed Without Authentication - -**Severity:** Critical -**CWE:** CWE-306 (Missing Authentication), CWE-200 (Exposure of Sensitive Information) -**Affected component:** Standalone API/UI - -**Evidence** - -- `hyperforge/src/hyperforge/standalone/settings.py:34-35` binds standalone mode to `0.0.0.0:8080` by default. -- `hyperforge/src/hyperforge/standalone/app.py:91-149` grants every request all application roles. Optional bearer validation applies only to recognized MCP paths. -- `hyperforge/src/hyperforge/standalone/app.py:237-242` mounts the UI router under the open authentication backend. -- `hyperforge/src/hyperforge/standalone/ui_router.py:127-134` returns the complete in-memory configuration using `model_dump()`. -- `hyperforge/src/hyperforge/standalone/ui_router.py:137-177` accepts, activates, and persists a replacement configuration without an authorization check. -- `hyperforge/src/hyperforge/standalone/settings.py:120-126` allows every CORS origin by default, increasing browser-based reachability where the service is network-accessible. - -**Impact** - -An unauthenticated client can retrieve configured API keys, OAuth client secrets, authorization headers, certificates, prompts, and workflow definitions. The client can replace live behavior, persist malicious configuration, consume paid APIs, and configure outbound requests for data exfiltration or SSRF. Because the full configuration is serialized directly, the database API's encrypted-field redaction is not applied. - -**Attack path** - -1. Request `GET /api/v1/ui/config` from a reachable standalone instance. -2. Extract credentials and internal endpoint configuration from the response. -3. Submit a modified document to `PUT /api/v1/ui/config` to change active workflows or outbound integrations. - -**Remediation** - -- Bind standalone mode to loopback by default. -- Require strong authentication and a distinct administrator permission for every `/api/v1/ui/*` endpoint that reads or mutates configuration. -- Disable configuration mutation by default in production. -- Define sensitive fields centrally and redact them from all responses, including custom headers, certificates, MCP environment values, and driver-specific credentials. -- Restrict CORS to explicit trusted origins. Do not combine wildcard origins with credentialed requests. -- Add audit logging for configuration reads and writes without logging configuration values. - -### HF-02: Internal Inspection Endpoint Returns Decrypted Credentials - -**Severity:** High -**CWE:** CWE-862 (Missing Authorization), CWE-200 (Exposure of Sensitive Information) -**Affected component:** Full FastAPI service - -**Evidence** - -- `hyperforge/src/hyperforge/api/internal/inspect.py:13-29` exposes `GET /api/internal/v1/agent/{kbid}` with a caller-controlled `account` query parameter and no authorization decorator. -- `hyperforge/src/hyperforge/api/app.py:77-83` mounts the internal router in the same application. -- `hyperforge/src/hyperforge/db/agents.py:451-480` decrypts marked fields before returning each driver. -- `hyperforge/src/hyperforge/api/models.py:79-84` includes the full `DriverConfig` list in the response. -- The normal management endpoint uses `dump_without_encrypted_fields()` at `hyperforge/src/hyperforge/api/v1/agents.py:160`; the inspection route does not. - -**Impact** - -Any caller that can reach the endpoint and identify or enumerate an account and agent can obtain decrypted driver credentials. The account value is supplied by the caller rather than derived from an authenticated identity. - -**Remediation** - -- Prefer removing the endpoint from the public application and exposing equivalent diagnostics through an authenticated internal service boundary. -- If retained, require service authentication and account-scoped authorization. -- Return only an allowlisted diagnostic projection and always remove encrypted fields after decryption. -- Add regression tests proving anonymous, cross-account, and ordinary member requests cannot access the route. - -### HF-03: SSRF Protection Bypass Through IPv6 or DNS Rebinding - -**Severity:** High -**CWE:** CWE-918 (Server-Side Request Forgery) -**Affected component:** Shared outbound HTTP transport and configurable URL consumers - -**Evidence** - -- `hyperforge/src/hyperforge/utils/http.py:17-30` resolves only `socket.AF_INET`, so AAAA records are not inspected. -- `hyperforge/src/hyperforge/utils/http.py:32-41` validates DNS before delegating to `httpx`, which resolves the hostname again when connecting. -- `agents/http/src/hyperforge_http/agent.py:36-68`, `agents/external/src/hyperforge_external/agent.py:64-117`, and `agents/mcp/src/hyperforge_mcp/http.py:454-490` consume configurable URLs through this transport. -- `agents/mcp/src/hyperforge_mcp/http.py:454-490` follows redirects; every redirect is checked by a new transport request, but each check still has the resolution race. -- `hyperforge/src/hyperforge/standalone/oauth.py:30-48` retrieves a configured JWKS URL without the shared SSRF transport. - -**Impact** - -A hostname can pass the IPv4 check while resolving to loopback or private IPv6 for the actual connection. DNS rebinding can also return a public address during validation and an internal address during connection. An attacker with configuration control can target cloud metadata, loopback services, cluster APIs, or internal data stores. HF-01 makes this attack unauthenticated in reachable standalone deployments. - -**Remediation** - -- Resolve all A and AAAA records and reject every address that is not explicitly permitted and globally routable. -- Connect to a validated address without a second uncontrolled DNS lookup while preserving the original hostname for TLS SNI and certificate validation. -- Revalidate redirect targets and restrict URL schemes to `https` or an explicit allowlist. -- Disable environment-proxy inheritance unless intentionally configured. -- Apply the same transport and policy to JWKS, OAuth metadata, and every configurable outbound endpoint. -- Consider destination allowlists for production rather than relying only on address classification. - -### HF-04: Docker Build Can Embed Local Secrets and Git Metadata - -**Severity:** High -**CWE:** CWE-200 (Exposure of Sensitive Information) -**Affected component:** `HYPERFORGE.Dockerfile` - -**Evidence** - -- No `.dockerignore` is present. -- `HYPERFORGE.Dockerfile:8` executes `COPY . /app/.`. -- `HYPERFORGE.Dockerfile:15` copies all of `/app` into the final image despite the comment stating that only the virtual environment is copied. - -**Impact** - -Docker sends the whole local build context unless excluded. Ignored `.env` files, credentials, `.git` history, test cassettes, caches, and developer artifacts can therefore be copied into image layers and the final image. Removing a file in a later layer would not reliably remove it from earlier layers. - -**Remediation** - -- Add a restrictive `.dockerignore` covering `.git`, environment files, credentials, caches, tests, local virtual environments, build outputs, and editor files. -- Copy manifests first, install dependencies, then copy only required source and built frontend assets. -- Copy the actual virtual environment and required runtime files into the final stage rather than all `/app`. -- Build and scan the resulting image, then inspect its filesystem and layer history before release. - -### HF-05: Unbounded MCP Request and Response Buffering - -**Severity:** Medium -**CWE:** CWE-400 (Uncontrolled Resource Consumption) -**Affected component:** Streamable MCP HTTP endpoint - -**Evidence** - -- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:382-392` accumulates all response chunks in a list. -- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:394-407` reads the complete request body into memory. -- `hyperforge/src/hyperforge/api/v1/mcp_interaction.py:425-428` joins all chunks, requiring another contiguous allocation. -- Standalone mode makes the route unauthenticated unless MCP auth is configured for the exact recognized path. - -**Impact** - -Large request bodies or generated MCP responses can exhaust process memory. Concurrent requests amplify the effect and can terminate the service. - -**Remediation** - -- Reject oversized requests at the reverse proxy and application boundary before calling `request.body()`. -- Stream requests and responses using the supported MCP session manager where possible. -- Cap total response bytes, chunk count, request duration, and concurrent MCP operations. -- Add load tests covering compressed and chunked bodies. - -### HF-06: HTTP Agents Accept Unbounded Remote Responses - -**Severity:** Medium -**CWE:** CWE-400 (Uncontrolled Resource Consumption) -**Affected component:** HTTP and external agents - -**Evidence** - -- `agents/http/src/hyperforge_http/agent.py:38-68` downloads and decodes the complete response. -- `agents/external/src/hyperforge_external/agent.py:118-131` decodes, logs, and stores the complete response. -- `agents/mcp/src/hyperforge_mcp/http.py:459-463` permits a 200-second default MCP timeout. - -**Impact** - -A malicious or compromised remote server can return an arbitrarily large or slowly streamed body, consuming worker memory and time. External-agent responses may also create very large logs and expose remote response data to centralized logging. - -**Remediation** - -- Stream responses and stop after a configurable maximum compressed and decompressed byte count. -- Apply connect, read, write, pool, and total operation timeouts. -- Truncate error bodies and avoid logging complete response bodies or model context. - -### HF-07: Runtime Container Executes as Root - -**Severity:** Medium -**CWE:** CWE-250 (Execution with Unnecessary Privileges) -**Affected component:** `HYPERFORGE.Dockerfile` - -**Evidence** - -- `HYPERFORGE.Dockerfile:14-22` does not create or select a non-root runtime user. -- The image also has no explicit `ENTRYPOINT`, `CMD`, or `HEALTHCHECK`. -- Base image `python:3.14`, bootstrap `uv`, and apt-installed npm are not digest/version pinned at `HYPERFORGE.Dockerfile:1-3,14`. - -**Impact** - -Application compromise provides root privileges inside the container and increases the consequences of mounted files, permissive runtime settings, or container-engine weaknesses. Mutable inputs reduce build reproducibility. - -**Remediation** - -- Create a dedicated UID/GID, ensure runtime files are owned appropriately, and set `USER` in the final stage. -- Use a minimal pinned base image and pin bootstrap tooling. -- Set an explicit startup command and health check or document that the orchestrator supplies them. -- Run with a read-only root filesystem, dropped Linux capabilities, no privilege escalation, resource limits, and a restrictive seccomp/AppArmor profile. - -### HF-08: Locked npm Dependencies Have Known Vulnerabilities - -**Severity:** Medium application risk; upstream advisories include High -**CWE:** CWE-1104 (Use of Unmaintained Third-Party Components) -**Affected component:** `hyperforge/frontend/package-lock.json` - -**Evidence** - -`npm audit --json` reported two high, one moderate, and three low vulnerabilities: - -| Package | Locked version | Advisory severity | Reachability assessment | -|---|---:|---:|---| -| `uuid` | 9.0.1 | High | Vulnerability affects v3/v5/v6 buffer APIs; project imports only v4 at `frontend/src/stores/chat.ts:3` and `workflow.ts:3` | -| `postcss` | 8.5.15 | High | Build-time transitive dependency; no user-controlled CSS processing found | -| `nanoid` | 3.3.12 | Moderate | Transitive build dependency; vulnerable custom size APIs are not called by project code | -| `esbuild` | 0.27.7 | Low | Development server issue affecting Windows | -| `vite` | 7.3.5 | Low | Inherits the esbuild advisory | -| `@vitejs/plugin-vue` | lockfile-resolved version | Low | Inherits the Vite advisory | - -`npm audit --omit=dev --json` still reported five advisories because the lockfile classifies much of the frontend graph as production dependencies. The production Python service serves compiled static assets and does not execute these npm packages at runtime. - -**Remediation** - -- Upgrade PostCSS/nanoid to patched versions through the Vite dependency graph. -- Upgrade `uuid` to a patched major version and run frontend tests/build checks. -- Keep Vite and plugin-vue aligned on supported versions. -- Add npm audit/SCA coverage to CI; the Black Duck workflow is configured for uv and its trigger does not match `package-lock.json`. - -### HF-09: MCP Client Certificate Temporary Files Are Not Deleted - -**Severity:** Low -**CWE:** CWE-459 (Incomplete Cleanup) -**Affected component:** MCP HTTP client - -**Evidence** - -- `agents/mcp/src/hyperforge_mcp/http.py:478-484` writes certificate material with `NamedTemporaryFile(delete=False)`. -- No subsequent unlink operation was found. - -**Impact** - -Private client certificates persist in the temporary directory and accumulate. Processes with equivalent privileges, diagnostic bundles, backups, or container snapshots may recover them. - -**Remediation** - -Delete the file immediately after `SSLContext.load_cert_chain()` in a `finally` block, enforce restrictive file permissions, and avoid disk-backed key material where the TLS library permits it. - -## Deployment-Dependent Risks - -### Trusted Header Authentication - -`hyperforge/src/hyperforge/api/authentication.py:33-94` accepts roles and users from `X-STF-*` and `X-NUCLIADB-*` headers without validating a signature or token. This is safe only if a trusted authorizer is the sole network peer and always strips client-supplied copies before injecting verified values. Direct API exposure would permit role spoofing. Enforce the trust boundary with network policy, mTLS, and proxy header sanitation; fail closed if the trusted authorizer is absent. - -### Restricted Python Isolation - -RestrictedPython is not an operating-system sandbox. Network verification is disabled by default in `agents/restricted/src/hyperforge_restricted/sandbox.py:25-42`, and debug execution can run in-process. No specific language escape was confirmed, but untrusted code should execute in a separate non-root container or microVM with no network, a read-only minimal filesystem, syscall restrictions, CPU/memory/process limits, and strict timeouts. - -### Frontend OAuth Link Scheme - -The frontend places an agent-provided OAuth URL directly into an anchor at `hyperforge/frontend/src/components/chat/ChatPanel.vue:72-77`, populated at `hyperforge/frontend/src/stores/chat.ts:184-190`. Vue escapes HTML, but URL schemes are not visibly allowlisted. Restrict links to `https:` and expected OAuth origins to prevent unsafe or deceptive schemes if a compromised integration supplies the URL. - -### Secret Classification Gaps - -Driver encryption covers declared `encrypted_fields`, but generic HTTP headers, external-agent headers, MCP certificates, and MCP stdio environment values are not consistently marked sensitive. Define a single secret-field policy, encrypt all persisted credentials, exclude them from API responses, and prevent them from entering logs or telemetry. - -## Positive Controls - -- SQL access reviewed uses SQLAlchemy expression construction and bound values; no concrete SQL injection was found. -- Runtime YAML loading uses `yaml.safe_load`; no pickle or equivalent unsafe deserialization was found. -- Vue templates reviewed use escaped interpolation rather than raw HTML rendering. -- Standalone JWT validation restricts algorithms and validates signature, expiration, optional not-before, issuer, audience, and scopes. -- The shared HTTP transport blocks IPv4 private and other non-global addresses, though HF-03 documents important gaps. -- Driver secret encryption uses Fernet and export encryption uses PBKDF2-derived keys. -- GitHub Actions references reviewed are pinned to full commit SHAs and checkout commonly disables credential persistence. -- Dedicated TruffleHog, Black Duck, Polaris, and zizmor workflows exist. -- `uv.lock` contains package hashes and `uv lock --check` passed. -- Common environment/credential files are ignored, and no tracked `.env`, private key, certificate, keystore, or Terraform state file was found. -- Renovate is configured in `.github/renovate.json`. - -## Scope and Method - -The review covered: - -- 22 Python workspace manifests and the root `uv.lock` (185 resolved packages). -- The Vue/Vite frontend manifest and lockfile. -- FastAPI HTTP, WebSocket, MCP, OAuth, standalone UI, authentication, database, encryption, and session code. -- HTTP, external, MCP, NucliaDB, Google, Perplexity, and restricted-code agent security boundaries. -- Dockerfile and 25 GitHub Actions workflows. -- Current-tree secret patterns and tracked sensitive filenames. -- Targeted searches for injection, process execution, deserialization, SSRF, XSS, file operations, and unbounded I/O. - -The worktree already contained uncommitted changes to: - -- `hyperforge/src/hyperforge/standalone/app.py` -- `hyperforge/src/hyperforge/standalone/settings.py` - -Those changes add configurable static-folder handling and were included in the reviewed state. They were not modified by this assessment. - -## Verification Results - -| Check | Result | -|---|---| -| `uv lock --check` | Passed; 185 packages resolved | -| `npm audit --json` | 6 advisories: 2 high, 1 moderate, 3 low | -| `npm audit --omit=dev --json` | 5 advisories: 2 high, 1 moderate, 2 low | -| `npm run build` | Passed with Vite 7.3.5 | -| `uv run pytest tests -q` in `agents/http` | 1 passed | -| `uv run pytest tests -q` in `agents/external` | 1 passed | -| `uv run pytest tests -q` in `agents/mcp` | 18 passed | -| `uv run pytest hyperforge/tests -q` | 159 passed; 48 setup errors because the Docker daemon was unavailable | -| Combined multi-package pytest invocation | Not valid due to duplicate `tests.conftest` module names; packages were rerun separately | -| Current-tree secret pattern scan | No confirmed production credential found | -| Tracked sensitive-file scan | No tracked matching files found | - -## Limitations - -- This was not a live penetration test and did not send exploit payloads to a deployed environment. -- Python advisory scanners (`pip-audit`, OSV Scanner), TruffleHog, Bandit, Semgrep, Syft, and Grype were not installed locally. Python vulnerabilities and full Git history secrets were therefore not independently queried. -- The Docker client was installed but the daemon was unavailable, so the image was not built or scanned and integration tests requiring PostgreSQL/Valkey containers could not start. -- Current-tree secret pattern matching cannot prove that all 304 repository commits are clean. -- Cloud, Kubernetes, reverse-proxy, firewall, mTLS, branch protection, GitHub environment, and secret-scope settings were not available for review. -- Dependency advisories describe affected versions; reachability judgments are based on static call-site review rather than runtime instrumentation. - -## Remediation Roadmap - -### Within 24 Hours - -- Restrict standalone and internal API network exposure. -- Fix HF-01 and HF-02 or disable the affected routes. -- Rotate exposed credentials where network exposure cannot be ruled out. - -### Within 7 Days - -- Correct SSRF handling across all configurable outbound requests. -- Add request/response limits for MCP and HTTP agents. -- Add `.dockerignore`, a least-privilege runtime user, and a minimal runtime image. -- Upgrade vulnerable frontend dependencies and add npm SCA to CI. - -### Within 30 Days - -- Run Python SCA, full-history secret scanning, SAST, and a built-image vulnerability scan in CI with blocking severity policies. -- Harden restricted-code execution with OS-level isolation. -- Add negative security tests for anonymous configuration access, internal route access, role-header spoofing, IPv6/private redirects, DNS rebinding, oversized payloads, and secret redaction. -- Review logging and telemetry for prompts, model context, external responses, OAuth data, and credentials. - -## Conclusion - -The project has several sound foundations, including parameterized database access, safe YAML parsing, encrypted driver fields, pinned GitHub Actions, and dedicated security workflows. However, HF-01 and HF-02 create direct credential-disclosure paths, and HF-03 can turn configuration control into internal network access. These issues should be treated as release blockers for any deployment reachable by untrusted clients. diff --git a/agents/mcp/src/hyperforge_mcp/agent.py b/agents/mcp/src/hyperforge_mcp/agent.py index 9e39d2e4..0e7ae3a3 100644 --- a/agents/mcp/src/hyperforge_mcp/agent.py +++ b/agents/mcp/src/hyperforge_mcp/agent.py @@ -201,7 +201,6 @@ async def get_tool_selection_prompt( }, tracking=memory.get_tracking_info(), ) - breakpoint() prompt_id: str = resp["prompt_id"] await memory.add_step( step_module=self.config.module, diff --git a/hyperforge/src/hyperforge/manager.py b/hyperforge/src/hyperforge/manager.py index 5ae73b3e..4ad85dc4 100644 --- a/hyperforge/src/hyperforge/manager.py +++ b/hyperforge/src/hyperforge/manager.py @@ -526,7 +526,6 @@ async def execute_json_reasoning( max_tokens: int = 8192, tracking: TrackingInfo | None = None, ) -> Tuple[Dict[str, Any], float, float, str | None]: - breakpoint() try: resp = await self.nua.generate( body=ChatModel( @@ -558,7 +557,6 @@ async def execute_json_reasoning( else: input_tokens = resp.consumption.normalized_tokens.input output_tokens = resp.consumption.normalized_tokens.output - breakpoint() return ( resp.object, input_tokens, From a35804a62da4debe326fdcf458648f92664c8c16 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Fri, 28 Aug 2026 13:17:57 +0200 Subject: [PATCH 3/9] Fix format --- hyperforge/src/hyperforge/api/v1/mcp_interaction.py | 4 +++- hyperforge/src/hyperforge/standalone/app.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hyperforge/src/hyperforge/api/v1/mcp_interaction.py b/hyperforge/src/hyperforge/api/v1/mcp_interaction.py index 8b427aab..9365fd38 100644 --- a/hyperforge/src/hyperforge/api/v1/mcp_interaction.py +++ b/hyperforge/src/hyperforge/api/v1/mcp_interaction.py @@ -319,7 +319,9 @@ async def interaction_mcp_handler( if int(content_length) > max_request_bytes: raise HTTPException(status_code=413, detail="MCP request is too large") except ValueError: - raise HTTPException(status_code=400, detail="Invalid Content-Length") from None + raise HTTPException( + status_code=400, detail="Invalid Content-Length" + ) from None body_bytes = bytearray() async for chunk in request.stream(): diff --git a/hyperforge/src/hyperforge/standalone/app.py b/hyperforge/src/hyperforge/standalone/app.py index a8c3e7fb..104c24c0 100644 --- a/hyperforge/src/hyperforge/standalone/app.py +++ b/hyperforge/src/hyperforge/standalone/app.py @@ -168,9 +168,9 @@ def _authenticate_ui(self, conn: HTTPConnection) -> None: if scheme.lower() != "basic" or not encoded: raise AuthenticationError("Invalid administrator credentials") try: - username, separator, password = base64.b64decode( - encoded, validate=True - ).decode("utf-8").partition(":") + username, separator, password = ( + base64.b64decode(encoded, validate=True).decode("utf-8").partition(":") + ) except (ValueError, UnicodeDecodeError): raise AuthenticationError("Invalid administrator credentials") from None if not separator or not ( From d12464b2836762d2a8caa860e185d33574cc4250 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 18:48:18 +0200 Subject: [PATCH 4/9] Clean Code --- .../src/hyperforge/api/internal/__init__.py | 4 --- .../src/hyperforge/api/internal/inspect.py | 30 ------------------- .../src/hyperforge/api/internal/router.py | 3 -- .../tests/unit/arag/test_internal_routes.py | 15 ---------- 4 files changed, 52 deletions(-) delete mode 100644 hyperforge/src/hyperforge/api/internal/__init__.py delete mode 100644 hyperforge/src/hyperforge/api/internal/inspect.py delete mode 100644 hyperforge/src/hyperforge/api/internal/router.py delete mode 100644 hyperforge/tests/unit/arag/test_internal_routes.py diff --git a/hyperforge/src/hyperforge/api/internal/__init__.py b/hyperforge/src/hyperforge/api/internal/__init__.py deleted file mode 100644 index f62ab884..00000000 --- a/hyperforge/src/hyperforge/api/internal/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import inspect -from .router import router - -__all__ = ["inspect", "router"] diff --git a/hyperforge/src/hyperforge/api/internal/inspect.py b/hyperforge/src/hyperforge/api/internal/inspect.py deleted file mode 100644 index 0f04171e..00000000 --- a/hyperforge/src/hyperforge/api/internal/inspect.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import TYPE_CHECKING - -from starlette.requests import Request - -from hyperforge.api.internal.router import router -from hyperforge.api.models import InspectData -from hyperforge.db.agents import AgentManager - -if TYPE_CHECKING: - from hyperforge.api.app import HTTPApplication - - -@router.get( - "/api/internal/v1/agent/{kbid}", - status_code=200, - description="Report task is done", - tags=["Task"], - include_in_schema=False, -) -async def inspect_agent_info(request: Request, kbid: str, account: str) -> InspectData: - app: HTTPApplication = request.app - agent_manager: AgentManager = app.agent_manager - - return InspectData( - contexts=await agent_manager.get_context(account, kbid), - driver=await agent_manager.get_drivers(account, kbid), - postprocess=await agent_manager.get_postprocess(account, kbid), - preprocess=await agent_manager.get_preprocess(account, kbid), - rules=await agent_manager.get_rules(account, kbid), - ) diff --git a/hyperforge/src/hyperforge/api/internal/router.py b/hyperforge/src/hyperforge/api/internal/router.py deleted file mode 100644 index 8009c643..00000000 --- a/hyperforge/src/hyperforge/api/internal/router.py +++ /dev/null @@ -1,3 +0,0 @@ -from fastapi.routing import APIRouter - -router = APIRouter() diff --git a/hyperforge/tests/unit/arag/test_internal_routes.py b/hyperforge/tests/unit/arag/test_internal_routes.py deleted file mode 100644 index d24f82bd..00000000 --- a/hyperforge/tests/unit/arag/test_internal_routes.py +++ /dev/null @@ -1,15 +0,0 @@ -from hyperforge.api.app import HTTPApplication -from hyperforge.api.settings import Settings -from hyperforge.db.settings import DataManagerSettings - - -def test_internal_inspection_route_is_not_mounted(): - app = HTTPApplication( - Settings(), - DataManagerSettings(postgresql_dsn="postgresql://unused"), - ) - - assert all( - getattr(route, "path", None) != "/api/internal/v1/agent/{kbid}" - for route in app.routes - ) From 4a695a6717e7b62a658bac70c59536beb77810b6 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 18:51:14 +0200 Subject: [PATCH 5/9] Fix --- agents/mcp/src/hyperforge_mcp/config_driver.py | 7 ------- hyperforge/src/hyperforge/standalone/settings.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/agents/mcp/src/hyperforge_mcp/config_driver.py b/agents/mcp/src/hyperforge_mcp/config_driver.py index 76014a86..bbed3344 100644 --- a/agents/mcp/src/hyperforge_mcp/config_driver.py +++ b/agents/mcp/src/hyperforge_mcp/config_driver.py @@ -18,13 +18,6 @@ def _redirect_uris_schema_default(schema: Dict[str, Any]) -> None: class MCPHTTPInnerConfig(EncryptedPayload): - encrypted_fields: ClassVar[list[str]] = [ - "client_secret", - "headers", - "ca_certificate", - "crt_certificate", - ] - uri: str timeout: float = 60 * 5 headers: Dict[str, str] = Field(default_factory=dict) diff --git a/hyperforge/src/hyperforge/standalone/settings.py b/hyperforge/src/hyperforge/standalone/settings.py index 6e9c651a..30615175 100644 --- a/hyperforge/src/hyperforge/standalone/settings.py +++ b/hyperforge/src/hyperforge/standalone/settings.py @@ -35,7 +35,7 @@ class StandaloneSettings(BaseSettings): # HTTP server # ------------------------------------------------------------------ - host: str = Field(default="127.0.0.1", description="Listen host.") + host: str = Field(default="0.0.0.0", description="Listen host.") port: int = Field(default=8080, description="Listen port.") log_level: str = Field(default="INFO", description="Log level (uvicorn + app).") debug: bool = Field(default=False, description="Enable debug mode.") From 1269666af2fa769f71f8aa802b4bb4d0872bbc02 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 18:53:37 +0200 Subject: [PATCH 6/9] Fix --- HYPERFORGE.Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HYPERFORGE.Dockerfile b/HYPERFORGE.Dockerfile index 9f9f39ba..457e32bb 100644 --- a/HYPERFORGE.Dockerfile +++ b/HYPERFORGE.Dockerfile @@ -34,3 +34,9 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)"] CMD ["hyperforge-api"] + +# OCI metadata: links this image to the GitHub repo as a package, with source + license. +LABEL org.opencontainers.image.source="https://github.com/nuclia/hyperforge" \ + org.opencontainers.image.description="Agentic workflow on top of NUA" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.authors="Nuclia Team" From 963223c418a365b3cc40fd7f45f77eaf0e03bf84 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 18:56:41 +0200 Subject: [PATCH 7/9] Fix --- hyperforge/src/hyperforge/utils/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hyperforge/src/hyperforge/utils/__init__.py b/hyperforge/src/hyperforge/utils/__init__.py index 715f0f00..b017db9b 100644 --- a/hyperforge/src/hyperforge/utils/__init__.py +++ b/hyperforge/src/hyperforge/utils/__init__.py @@ -17,7 +17,7 @@ def iterate_tools_resp( yield tool.function.name, used_params -def sync_check_dns(url: str) -> Optional[str]: +def validate_url(url: str) -> Optional[str]: parsed_url = urllib.parse.urlparse(url) if parsed_url.scheme.lower() not in {"http", "https"}: return "URL must use HTTP or HTTPS" @@ -29,16 +29,12 @@ def sync_check_dns(url: str) -> Optional[str]: def sync_dns_validation(url: str) -> str: - error = sync_check_dns(url) + error = validate_url(url) if error is not None: raise ValueError(error) return url -async def check_dns(url: str) -> str: - return sync_dns_validation(url) - - class WidgetType(str, Enum): """ Enumeration of available widget types for form field rendering. From 46672424dc3f89ba9865a9c0e75699f3aea1ccbb Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 18:59:52 +0200 Subject: [PATCH 8/9] Fix --- hyperforge/src/hyperforge/utils/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperforge/src/hyperforge/utils/http.py b/hyperforge/src/hyperforge/utils/http.py index bdf4093c..c79edbbc 100644 --- a/hyperforge/src/hyperforge/utils/http.py +++ b/hyperforge/src/hyperforge/utils/http.py @@ -92,7 +92,7 @@ async def sleep(self, seconds: float) -> None: class SafeTransport(AsyncHTTPTransport): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._pool._network_backend = SafeNetworkBackend() + self._pool._network_backend = SafeNetworkBackend() # type: ignore async def handle_async_request(self, request: Request) -> Response: validate_public_http_url(str(request.url)) From 61ab1ad31ed8a5d7a26757ab79a3fe16c3271d66 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 31 Aug 2026 19:07:36 +0200 Subject: [PATCH 9/9] fix --- hyperforge/tests/standalone/test_standalone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperforge/tests/standalone/test_standalone.py b/hyperforge/tests/standalone/test_standalone.py index 7852fea8..9721ff28 100644 --- a/hyperforge/tests/standalone/test_standalone.py +++ b/hyperforge/tests/standalone/test_standalone.py @@ -247,7 +247,7 @@ async def test_standalone_ui_requires_admin_credentials(local_agents_config): async def test_standalone_security_defaults(): settings = StandaloneSettings() - assert settings.host == "127.0.0.1" + assert settings.host == "0.0.0.0" assert settings.cors_allow_origin == []