Add Linux account workspace runtime
This commit is contained in:
@@ -40,6 +40,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
|
||||
@@ -162,6 +163,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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -192,6 +192,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
|
||||
@@ -256,6 +257,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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user