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
+2
View File
@@ -41,6 +41,7 @@ class ToolExecutionContext:
permissions: AgentPermissions
scratchpad_directory: Path | None = None
python_env_dir: Path | None = None
runtime_user: str | None = None
extra_env: dict[str, str] = field(default_factory=dict)
cancel_event: Any | None = None
process_registry: Any | None = None
@@ -169,6 +170,7 @@ def build_tool_context(
if config.python_env_dir
else None
),
runtime_user=config.runtime_user,
extra_env=dict(extra_env or {}),
cancel_event=cancel_event,
process_registry=process_registry,
+56 -3
View File
@@ -6,6 +6,7 @@ import hashlib
import io
import json
import os
import pwd
import re
import secrets
import selectors
@@ -1648,6 +1649,8 @@ def _resolve_path(
else context.root / expanded
)
resolved = candidate.resolve(strict=not allow_missing)
if _is_session_workspace_path(resolved, context):
return resolved
try:
resolved.relative_to(context.root)
except ValueError as exc:
@@ -1678,6 +1681,17 @@ def _session_logical_path(path: Path, context: ToolExecutionContext) -> Path | N
return None
def _is_session_workspace_path(path: Path, context: ToolExecutionContext) -> bool:
if context.scratchpad_directory is None:
return False
session_root = context.scratchpad_directory.parent.resolve(strict=False)
try:
path.resolve(strict=False).relative_to(session_root)
return True
except ValueError:
return False
def _display_path(path: Path, context: ToolExecutionContext) -> str:
resolved = path.resolve()
try:
@@ -1691,10 +1705,25 @@ def _execution_cwd(context: ToolExecutionContext) -> Path:
return Path(context.jupyter_runtime.binding.workspace_cwd)
if context.scratchpad_directory is not None:
context.scratchpad_directory.mkdir(parents=True, exist_ok=True)
_chown_to_runtime_user(context.scratchpad_directory, context)
return context.scratchpad_directory
return context.root
def _chown_to_runtime_user(path: Path, context: ToolExecutionContext) -> None:
runtime_user = (context.runtime_user or '').strip()
if not runtime_user or os.name != 'posix':
return
try:
user_info = pwd.getpwnam(runtime_user)
except KeyError:
return
try:
os.chown(path, user_info.pw_uid, user_info.pw_gid)
except (FileNotFoundError, PermissionError, OSError):
return
def _is_platform_app_root(root: Path) -> bool:
return all((root / marker).exists() for marker in _PLATFORM_ROOT_MARKERS)
@@ -1873,8 +1902,10 @@ def _write_file(arguments: dict[str, Any], context: ToolExecutionContext) -> str
elif target.exists():
raise ToolExecutionError(f'Path exists and is not a file: {target}')
target.parent.mkdir(parents=True, exist_ok=True)
_chown_to_runtime_user(target.parent, context)
final_text = (previous_text or '') + content if append else content
target.write_text(final_text, encoding='utf-8')
_chown_to_runtime_user(target, context)
rel = _display_path(target, context)
new_sha256 = hashlib.sha256(final_text.encode('utf-8')).hexdigest()
action = 'append_file' if append else 'write_file'
@@ -1994,6 +2025,7 @@ def _edit_file(arguments: dict[str, Any], context: ToolExecutionContext) -> str:
before_sha256 = hashlib.sha256(current.encode('utf-8')).hexdigest()
updated = current.replace(old_text, new_text) if replace_all else current.replace(old_text, new_text, 1)
target.write_text(updated, encoding='utf-8')
_chown_to_runtime_user(target, context)
rel = _display_path(target, context)
replaced = occurrences if replace_all else 1
after_sha256 = hashlib.sha256(updated.encode('utf-8')).hexdigest()
@@ -2078,6 +2110,7 @@ def _notebook_edit(arguments: dict[str, Any], context: ToolExecutionContext) ->
cell.setdefault('execution_count', None)
updated = json.dumps(notebook, ensure_ascii=True, indent=1) + '\n'
target.write_text(updated, encoding='utf-8')
_chown_to_runtime_user(target, context)
after_sha256 = hashlib.sha256(updated.encode('utf-8')).hexdigest()
rel = _display_path(target, context)
return (
@@ -2367,7 +2400,7 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
stderr=subprocess.PIPE,
text=True,
env=_build_subprocess_env(context),
**_subprocess_isolation_kwargs(),
**_subprocess_execution_kwargs(context),
)
_register_child_process(context, process)
if stdin and process.stdin is not None:
@@ -2485,6 +2518,24 @@ def _subprocess_isolation_kwargs() -> dict[str, Any]:
return {}
def _subprocess_execution_kwargs(context: ToolExecutionContext) -> dict[str, Any]:
runtime_user = (context.runtime_user or '').strip()
if not runtime_user or os.name != 'posix':
return _subprocess_isolation_kwargs()
try:
user_info = pwd.getpwnam(runtime_user)
except KeyError as exc:
raise ToolExecutionError(f'Linux runtime user not found: {runtime_user}') from exc
def demote() -> None:
os.setsid()
os.initgroups(runtime_user, user_info.pw_gid)
os.setgid(user_info.pw_gid)
os.setuid(user_info.pw_uid)
return {'preexec_fn': demote}
def _terminate_process(process: subprocess.Popen[Any]) -> None:
if process.poll() is not None:
return
@@ -2619,6 +2670,7 @@ def _run_python_package(arguments: dict[str, Any], context: ToolExecutionContext
text=True,
timeout=timeout_seconds,
env=_build_subprocess_env(context),
**_subprocess_execution_kwargs(context),
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout if isinstance(exc.stdout, str) else ''
@@ -2740,6 +2792,7 @@ def _run_bash(arguments: dict[str, Any], context: ToolExecutionContext) -> str:
text=True,
timeout=context.command_timeout_seconds,
env=_build_subprocess_env(context),
**_subprocess_execution_kwargs(context),
)
stdout = completed.stdout or ''
stderr = completed.stderr or ''
@@ -4344,13 +4397,13 @@ def _stream_bash(
command,
shell=True,
executable='/bin/bash',
cwd=context.root,
cwd=_execution_cwd(context),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env=_build_subprocess_env(context),
**_subprocess_isolation_kwargs(),
**_subprocess_execution_kwargs(context),
)
_register_child_process(context, process)
except (ToolPermissionError, ToolExecutionError, OSError, subprocess.SubprocessError) as exc:
+1
View File
@@ -171,6 +171,7 @@ class AgentRuntimeConfig:
session_directory: Path = field(default_factory=lambda: (Path('.port_sessions') / 'agent').resolve())
scratchpad_root: Path = field(default_factory=lambda: (Path('.port_sessions') / 'scratchpad').resolve())
python_env_dir: Path | None = None
runtime_user: str | None = None
enabled_skill_names: tuple[str, ...] | None = None
+6
View File
@@ -194,6 +194,7 @@ def serialize_runtime_config(runtime_config: AgentRuntimeConfig) -> JSONDict:
if runtime_config.python_env_dir is not None
else None
),
'runtime_user': runtime_config.runtime_user,
'enabled_skill_names': (
list(runtime_config.enabled_skill_names)
if runtime_config.enabled_skill_names is not None
@@ -258,6 +259,11 @@ def deserialize_runtime_config(payload: JSONDict) -> AgentRuntimeConfig:
if payload.get('python_env_dir') is not None
else None
),
runtime_user=(
str(payload['runtime_user'])
if payload.get('runtime_user') is not None
else None
),
enabled_skill_names=enabled_skill_names,
)