Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ jobs:
files: |
gh-release-assets/*

# Extract version from tag (e.g., v0.0.85 -> 0.0.89)
# Extract version from tag (e.g., v0.0.89 -> 0.0.89)
- name: Extract version
if: startsWith(github.ref, 'refs/tags/')
id: version
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,61 @@ permissions:
contents: read

jobs:
web-local-smoke:
name: Run Web + Local Brain Smoke
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install frontend dependencies
run: npm install --ignore-scripts

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.11

- name: Install backend dependencies
run: |
cd backend
uv sync

- name: Run web + local brain smoke
run: bash scripts/smoke-web-local-brain.sh

frontend-quality:
name: Run Frontend Guardrails
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install frontend dependencies
run: npm install --ignore-scripts

- name: Run type check
run: npm run type-check

- name: Check Electron Access Guard
run: bash scripts/check-electron-access.sh

pytest:
name: Run Python Tests
runs-on: ubuntu-latest
Expand Down
35 changes: 35 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
```bash
# Option 1: Start with uvicorn directly
uv run uvicorn main:api --port 5001

# Option 2: Standalone mode (no Electron dependency)
uv run python main.py

# Option 3: If uv run hangs, delete lock files and retry, or use venv directly:
.venv/bin/python main.py
# or
.venv/bin/uvicorn main:api --port 5001 --host 0.0.0.0

# If uv hangs, delete lock files first: rm -f uv_installing.lock uv_installed.lock
```

### Environment Variables (Standalone)

| Variable | Default | Description |
| ---------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
| `EIGENT_BRAIN_PORT` | 5001 | Listening port |
| `EIGENT_BRAIN_HOST` | 0.0.0.0 | Listening address |
| `EIGENT_DEBUG` | - | Set to 1/true to enable reload |
| `EIGENT_WORKSPACE` | ~/.eigent/workspace | Working directory |
| `EIGENT_DEPLOYMENT_TYPE` | (auto) | `local` / `cloud_vm` / `sandbox` / `docker`; determines Hands capabilities (see ADR-0006) |
| `EIGENT_HANDS_MODE` | - | Set to `remote` to enable `RemoteHands` (remote cluster resource mode) |
| `EIGENT_HANDS_CLUSTER_CONFIG_FILE` | - | Path to `RemoteHands` config file (TOML); **recommended** |
| `EIGENT_HANDS_TERMINAL` | - | Override terminal hand: `1`/`true`/`yes` or `0`/`false`/`no` |
| `EIGENT_HANDS_BROWSER` | - | Override browser hand |
| `EIGENT_HANDS_FILESYSTEM` | - | Override filesystem scope: `full` / `workspace_only` |
| `EIGENT_HANDS_MCP` | - | Override MCP mode: `all` / `allowlist` |

RemoteHands config file example:

```bash
cp backend/config/hands_clusters.example.toml ~/.eigent/hands_clusters.toml
export EIGENT_HANDS_MODE=remote
export EIGENT_HANDS_CLUSTER_CONFIG_FILE=~/.eigent/hands_clusters.toml
```

i18n operation process: https://github.com/Anbarryprojects/fastapi-babel
Expand Down
57 changes: 54 additions & 3 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,68 @@
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import os

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware

# Initialize FastAPI with title
api = FastAPI(title="Eigent Multi-Agent System API")

# Add CORS middleware

@api.get("/")
def root():
"""Root endpoint - confirms this is the Brain backend."""
return {"service": "eigent-brain", "docs": "/docs", "health": "/health"}


_cors_raw = os.environ.get("EIGENT_CORS_ORIGINS", "")
_allowed_origins = [o.strip() for o in _cors_raw.split(",") if o.strip()]
_default_frame_ancestors = [
"'self'",
"http://localhost:*",
"http://127.0.0.1:*",
"https://localhost:*",
"https://127.0.0.1:*",
]
_frame_ancestors = " ".join(
dict.fromkeys(
[
*_default_frame_ancestors,
*[origin for origin in _allowed_origins if origin != "*"],
]
)
)


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
if request.url.path.startswith("/files/preview/"):
if "X-Frame-Options" in response.headers:
del response.headers["X-Frame-Options"]
response.headers["Content-Security-Policy"] = (
f"frame-ancestors {_frame_ancestors};"
)
else:
response.headers["X-Frame-Options"] = "DENY"
return response


api.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_origins=_allowed_origins or ["*"],
allow_credentials=bool(_allowed_origins),
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Session-ID"],
)
api.add_middleware(SecurityHeadersMiddleware)

# Phase 2: Channel/Session header parsing (X-Channel, X-Session-ID, X-User-ID)
from app.router_layer import ChannelSessionMiddleware

api.add_middleware(ChannelSessionMiddleware)
Loading
Loading