Add Linux account workspace runtime

This commit is contained in:
wuyang6
2026-05-20 12:29:11 +08:00
parent 5684df3caf
commit ee596a33e0
11 changed files with 1178 additions and 37 deletions
+56 -3
View File
@@ -6,6 +6,7 @@ import hashlib
import io
import json
import os
import pwd
import re
import selectors
import shutil
@@ -1644,6 +1645,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:
@@ -1674,6 +1677,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:
@@ -1687,10 +1701,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)
@@ -1886,8 +1915,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'
@@ -2007,6 +2038,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()
@@ -2091,6 +2123,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 (
@@ -2360,7 +2393,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:
@@ -2482,6 +2515,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
@@ -2616,6 +2667,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 ''
@@ -2729,6 +2781,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 ''
@@ -4063,13 +4116,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: