mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 12:44:45 +00:00
244 lines
8.5 KiB
Python
244 lines
8.5 KiB
Python
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
"""
|
|
BrainCapabilities — Brain capability set, determined by deployment env. Everything revolves around what the Brain can operate.
|
|
|
|
Hand Types (capability dimensions — what the Brain can reach and control):
|
|
- filesystem: operate local files (scope: full | workspace_only | none)
|
|
- terminal: execute shell commands
|
|
- browser: control browser (CDP)
|
|
- mcp: use MCP tool protocol (all | allowlist)
|
|
|
|
Design principles:
|
|
- Brain on local/cloud VM -> full capabilities (extensible: smart home, router, car, etc.)
|
|
- Brain in sandbox/Docker -> limited capabilities
|
|
- Channel only affects message display format (Markdown/plain/Block Kit), does not determine Brain capabilities
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from app.component.environment import env
|
|
|
|
logger = logging.getLogger("hands.capabilities")
|
|
|
|
# Deployment determines Brain capabilities
|
|
DEPLOYMENT_FULL = ("local", "cloud_vm", "") # full capabilities
|
|
DEPLOYMENT_SANDBOX = ("sandbox", "docker", "container") # limited capabilities
|
|
|
|
|
|
def _is_running_in_docker() -> bool:
|
|
"""Detect if Brain runs inside Docker/container."""
|
|
if Path("/.dockerenv").exists():
|
|
return True
|
|
try:
|
|
cgroup = Path("/proc/1/cgroup").read_text()
|
|
return (
|
|
"docker" in cgroup
|
|
or "containerd" in cgroup
|
|
or "kubepods" in cgroup
|
|
)
|
|
except (OSError, FileNotFoundError):
|
|
return False
|
|
|
|
|
|
def _probe_cdp_browser() -> bool:
|
|
"""Check if CDP browser is configured/available."""
|
|
if os.environ.get("EIGENT_CDP_URL"):
|
|
return True
|
|
cdp_json = Path.home() / ".eigent" / "cdp.json"
|
|
if cdp_json.exists():
|
|
return True
|
|
# Electron persists CDP pool here; if present, browser capability is likely available.
|
|
cdp_pool = Path.home() / ".eigent" / "cdp-browsers.json"
|
|
return cdp_pool.exists()
|
|
|
|
|
|
def _is_electron_runtime() -> bool:
|
|
"""Detect whether Brain is launched by Electron desktop host."""
|
|
return env("EIGENT_RUNTIME", "").lower().strip() == "electron"
|
|
|
|
|
|
def _can_launch_local_cdp_browser() -> bool:
|
|
"""Check if local runtime can provision a CDP browser on demand."""
|
|
if os.environ.get("EIGENT_BRAIN_LAUNCH_BROWSER", "true").lower() in (
|
|
"false",
|
|
"0",
|
|
"no",
|
|
):
|
|
return False
|
|
try:
|
|
from app.utils.browser_launcher import _find_chrome_executable
|
|
|
|
return _find_chrome_executable() is not None
|
|
except Exception as e:
|
|
logger.debug(f"Could not probe local browser executable: {e}")
|
|
return False
|
|
|
|
|
|
@dataclass
|
|
class BrainCapabilities:
|
|
"""
|
|
Brain capability set (detected + config), determined at startup, global singleton.
|
|
|
|
Each field maps to a Hand Type: what the Brain can operate.
|
|
"""
|
|
|
|
has_terminal: bool = True
|
|
"""terminal hand: can execute shell"""
|
|
|
|
has_browser: bool = False
|
|
"""browser hand: can control CDP browser"""
|
|
|
|
filesystem_scope: str = "full"
|
|
"""filesystem hand: full | workspace_only | none"""
|
|
|
|
mcp_mode: str = "all"
|
|
"""mcp hand: all | allowlist"""
|
|
|
|
mcp_allowlist: list[str] = field(default_factory=list)
|
|
"""used when mcp_mode=allowlist"""
|
|
|
|
workspace_root: Path = field(
|
|
default_factory=lambda: Path("~/.eigent/workspace").expanduser()
|
|
)
|
|
"""workspace root path"""
|
|
|
|
deployment_type: str = "local"
|
|
"""deployment type (for logging): local | cloud_vm | sandbox | docker"""
|
|
|
|
@property
|
|
def mode(self) -> str:
|
|
"""capability tier: full | sandbox — for IHands.mode compatibility"""
|
|
return "full" if self._is_full else "sandbox"
|
|
|
|
@property
|
|
def _is_full(self) -> bool:
|
|
return self.filesystem_scope == "full" and self.has_terminal
|
|
|
|
|
|
def detect_capabilities(config: dict | None = None) -> BrainCapabilities:
|
|
"""
|
|
Detect Brain capabilities, two-layer decision:
|
|
1. Deployment env: EIGENT_DEPLOYMENT_TYPE / Docker auto-detect
|
|
2. Env var overrides: EIGENT_HANDS_*
|
|
"""
|
|
cfg = config or {}
|
|
|
|
# 1. Deployment env determines base capabilities
|
|
deployment = env("EIGENT_DEPLOYMENT_TYPE") or ""
|
|
deployment = deployment.lower().strip()
|
|
|
|
if deployment in DEPLOYMENT_FULL:
|
|
# local/cloud VM -> full capabilities
|
|
in_docker = _is_running_in_docker()
|
|
if in_docker:
|
|
logger.info("Brain running in Docker, using limited capabilities")
|
|
deployment = "docker"
|
|
caps = BrainCapabilities(
|
|
has_terminal=shutil.which("bash") is not None,
|
|
has_browser=False,
|
|
filesystem_scope="workspace_only",
|
|
mcp_mode="all", # MCP available in all deployment modes
|
|
workspace_root=Path(
|
|
env("EIGENT_WORKSPACE", "~/.eigent/workspace")
|
|
).expanduser(),
|
|
deployment_type="docker",
|
|
)
|
|
else:
|
|
# local/desktop: browser hand when CDP is configured/reachable,
|
|
# Electron host is present, or local browser can be provisioned.
|
|
has_browser = _probe_cdp_browser()
|
|
if not has_browser and _is_electron_runtime():
|
|
has_browser = True
|
|
if not has_browser:
|
|
has_browser = _can_launch_local_cdp_browser()
|
|
if not has_browser:
|
|
logger.warning(
|
|
"Browser capability disabled: no CDP config, "
|
|
"not running under Electron host, and no launchable browser found."
|
|
)
|
|
caps = BrainCapabilities(
|
|
has_terminal=shutil.which("bash") is not None,
|
|
has_browser=has_browser,
|
|
filesystem_scope="full",
|
|
mcp_mode="all",
|
|
workspace_root=Path(
|
|
env("EIGENT_WORKSPACE", "~/.eigent/workspace")
|
|
).expanduser(),
|
|
deployment_type="cloud_vm"
|
|
if deployment == "cloud_vm"
|
|
else "local",
|
|
)
|
|
else:
|
|
# sandbox / docker / container -> limited capabilities
|
|
caps = BrainCapabilities(
|
|
has_terminal=shutil.which("bash") is not None,
|
|
has_browser=False,
|
|
filesystem_scope="workspace_only",
|
|
mcp_mode="all", # MCP available in all deployment modes
|
|
workspace_root=Path(
|
|
env("EIGENT_WORKSPACE", "~/.eigent/workspace")
|
|
).expanduser(),
|
|
deployment_type=deployment or "sandbox",
|
|
)
|
|
|
|
# 2. Env var overrides
|
|
if env("EIGENT_HANDS_TERMINAL") is not None:
|
|
caps.has_terminal = env("EIGENT_HANDS_TERMINAL", "true").lower() in (
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
)
|
|
if env("EIGENT_HANDS_BROWSER") is not None:
|
|
caps.has_browser = env("EIGENT_HANDS_BROWSER", "false").lower() in (
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
)
|
|
if env("EIGENT_HANDS_FILESYSTEM") is not None:
|
|
caps.filesystem_scope = env("EIGENT_HANDS_FILESYSTEM", "full")
|
|
if env("EIGENT_HANDS_MCP") is not None:
|
|
caps.mcp_mode = env("EIGENT_HANDS_MCP", "all")
|
|
if env("EIGENT_CDP_URL"):
|
|
caps.has_browser = True
|
|
|
|
# 3. Config file overrides
|
|
if "terminal" in cfg:
|
|
caps.has_terminal = bool(cfg["terminal"])
|
|
if "browser" in cfg:
|
|
caps.has_browser = bool(cfg["browser"])
|
|
if "filesystem" in cfg:
|
|
caps.filesystem_scope = str(cfg["filesystem"])
|
|
if "mcp" in cfg:
|
|
caps.mcp_mode = str(cfg["mcp"])
|
|
if "mcp_allowlist" in cfg:
|
|
caps.mcp_allowlist = list(cfg["mcp_allowlist"])
|
|
|
|
logger.info(
|
|
"BrainCapabilities detected",
|
|
extra={
|
|
"deployment": caps.deployment_type,
|
|
"mode": caps.mode,
|
|
"terminal": caps.has_terminal,
|
|
"browser": caps.has_browser,
|
|
"filesystem": caps.filesystem_scope,
|
|
"mcp": caps.mcp_mode,
|
|
},
|
|
)
|
|
return caps
|