Merge remote-tracking branch 'origin/main' into wsh_dev

# Conflicts:
#	backend/api/server.py
This commit is contained in:
hupenglong1
2026-05-25 11:46:44 +08:00
77 changed files with 11155 additions and 2259 deletions
+157 -2
View File
@@ -13,6 +13,7 @@ import hashlib
import shlex
import json
import os
import pwd
import queue
import re
import shutil
@@ -76,6 +77,9 @@ from src.session_store import (
)
from src.token_budget import calculate_token_budget
LINUX_ACCOUNT_WORKSPACE_NAME = 'zk-agent'
LINUX_ACCOUNT_ENV = 'CLAW_ENABLE_LINUX_ACCOUNTS'
STATIC_DIR = Path(__file__).resolve().parents[2] / 'frontend' / 'legacy-static'
API_TOOL_CONTENT_MAX_CHARS = 20000
@@ -638,6 +642,8 @@ class AgentState:
if not account_id:
return self.session_directory.parent
safe_id = _safe_account_id(account_id)
if _linux_accounts_enabled():
return _linux_account_workspace(safe_id).resolve()
return (self.session_directory.parent / 'accounts' / safe_id).resolve()
def account_paths(self, account_id: str | None) -> dict[str, Path]:
@@ -837,7 +843,15 @@ class AgentState:
for directory in paths.values():
if directory.name != '.venv':
directory.mkdir(parents=True, exist_ok=True)
self._ensure_python_env(paths['python_env'])
runtime_user = (
_safe_account_id(account_id)
if account_id and _linux_accounts_enabled()
else None
)
if runtime_user:
_ensure_linux_user_exists(runtime_user)
_chown_path_for_linux_user(paths['base'], runtime_user, recursive=True)
self._ensure_python_env(paths['python_env'], account_id)
permissions = AgentPermissions(
allow_file_write=config.allow_write,
allow_shell_commands=config.allow_shell,
@@ -849,6 +863,7 @@ class AgentState:
session_directory=paths['sessions'],
scratchpad_root=paths['scratchpad'],
python_env_dir=paths['python_env'],
runtime_user=runtime_user,
enabled_skill_names=self.enabled_skill_names(account_id),
auto_compact_threshold_tokens=180_000,
)
@@ -949,12 +964,37 @@ class AgentState:
def lock(self) -> Lock:
return self._lock
def _ensure_python_env(self, env_dir: Path) -> None:
def _ensure_python_env(self, env_dir: Path, account_id: str | None = None) -> None:
python_bin = env_dir / 'bin' / 'python'
pip_bin = env_dir / 'bin' / 'pip'
if python_bin.exists() and pip_bin.exists():
return
env_dir.parent.mkdir(parents=True, exist_ok=True)
runtime_user = _safe_account_id(account_id) if account_id and _linux_accounts_enabled() else None
if runtime_user:
_ensure_linux_user_exists(runtime_user)
env_dir.parent.mkdir(parents=True, exist_ok=True)
_chown_path_for_linux_user(env_dir.parent, runtime_user, recursive=True)
runtime_python = os.environ.get('CLAW_RUNTIME_PYTHON_BIN') or shutil.which('python3')
if not runtime_python:
raise RuntimeError('Linux runtime requires python3 or CLAW_RUNTIME_PYTHON_BIN')
subprocess.run(
[
'runuser',
'-u',
runtime_user,
'--',
runtime_python,
'-m',
'venv',
str(env_dir),
],
check=True,
capture_output=True,
text=True,
timeout=120,
)
return
venv.EnvBuilder(with_pip=True, symlinks=False).create(env_dir)
@@ -4538,6 +4578,107 @@ def _account_id_from_email(email: str | None) -> str | None:
return prefix
def _linux_accounts_enabled() -> bool:
return os.environ.get(LINUX_ACCOUNT_ENV, '').strip().lower() in {
'1',
'true',
'yes',
'on',
}
def _linux_account_workspace(account_id: str) -> Path:
return Path('/home') / account_id / LINUX_ACCOUNT_WORKSPACE_NAME
def _validate_linux_username(account_id: str) -> None:
if not re.fullmatch(r'[a-z_][a-z0-9_-]{1,31}', account_id):
raise HTTPException(
status_code=400,
detail='启用 Linux 账号时,账号名必须以小写字母或下划线开头,只包含小写字母、数字、下划线或中划线,长度 2-32',
)
def _ensure_linux_account(account_id: str, password: str) -> None:
_validate_linux_username(account_id)
if os.geteuid() != 0:
raise HTTPException(
status_code=500,
detail='CLAW_ENABLE_LINUX_ACCOUNTS=1 需要后端以 root 身份运行',
)
try:
pwd.getpwnam(account_id)
except KeyError:
subprocess.run(
[
'useradd',
'-m',
'-d',
f'/home/{account_id}',
'-s',
'/bin/bash',
account_id,
],
check=True,
capture_output=True,
text=True,
timeout=30,
)
chpasswd = subprocess.run(
['chpasswd'],
input=f'{account_id}:{password}\n',
check=False,
capture_output=True,
text=True,
timeout=30,
)
if chpasswd.returncode != 0:
detail = (chpasswd.stderr or chpasswd.stdout or '').strip()
raise HTTPException(status_code=500, detail=f'同步 Linux 用户密码失败:{detail}')
workspace = _linux_account_workspace(account_id)
for child in (
workspace / 'sessions',
workspace / 'python',
workspace / 'memory',
workspace / 'integrations',
):
child.mkdir(parents=True, exist_ok=True)
_chown_path_for_linux_user(workspace, account_id, recursive=True)
def _ensure_linux_user_exists(account_id: str) -> None:
try:
pwd.getpwnam(account_id)
except KeyError as exc:
raise RuntimeError(
f'Linux runtime user does not exist: {account_id}. '
'Create the account through the platform first, or disable CLAW_ENABLE_LINUX_ACCOUNTS.'
) from exc
def _chown_path_for_linux_user(path: Path, account_id: str, *, recursive: bool = False) -> None:
if os.name != 'posix':
return
try:
user_info = pwd.getpwnam(account_id)
except KeyError:
return
target = path.resolve(strict=False)
try:
os.chown(target, user_info.pw_uid, user_info.pw_gid)
except PermissionError:
return
except FileNotFoundError:
return
if not recursive or not target.is_dir():
return
for child in target.rglob('*'):
try:
os.chown(child, user_info.pw_uid, user_info.pw_gid)
except (PermissionError, FileNotFoundError):
continue
def _admin_token() -> str:
return os.environ.get('ZK_ADMIN_TOKEN') or 'admin'
@@ -4686,6 +4827,8 @@ def _admin_create_account(state: AgentState, account_id: str) -> dict[str, Any]:
safe_id = _safe_account_id(account_id)
if safe_id == 'default':
raise HTTPException(status_code=400, detail='账号名不合法')
if _linux_accounts_enabled():
_validate_linux_username(safe_id)
users = _load_users_file(state)
user_rows = users.setdefault('users', [])
if any(
@@ -4694,6 +4837,8 @@ def _admin_create_account(state: AgentState, account_id: str) -> dict[str, Any]:
for item in user_rows
):
raise HTTPException(status_code=400, detail='账号已存在')
if _linux_accounts_enabled():
_ensure_linux_account(safe_id, '123456')
user_rows.append(
{
'id': safe_id,
@@ -4737,8 +4882,18 @@ def _admin_delete_account(state: AgentState, account_id: str) -> dict[str, Any]:
encoding='utf-8',
)
base = _accounts_root(state) / safe_id
if _linux_accounts_enabled():
base = _linux_account_workspace(safe_id)
if base.exists():
shutil.rmtree(base)
if _linux_accounts_enabled():
subprocess.run(
['passwd', '-l', safe_id],
check=False,
capture_output=True,
text=True,
timeout=30,
)
state._clear_agents_for_account(safe_id)
return {'account_id': safe_id, 'deleted': True}