fix(backends,storage,core): setuid in exec wrapper, missing-column migration, runner config dir for mirror, tagged prompt sources

This commit is contained in:
hh
2026-08-28 02:10:19 +02:00
parent e1f242a87a
commit 50b7057fa4
6 changed files with 127 additions and 23 deletions
+17 -9
View File
@@ -13,8 +13,9 @@ Events on the wire are the Anthropic ``MessageStreamEvent`` family: one
rebased across the API calls claude makes inside the turn.
Process isolation: claude is spawned through a small exec wrapper that
drops every inherited environment variable outside a whitelist, and,
when ``RunnerConfig.user`` is set, under that uid.
drops every inherited environment variable outside a whitelist and, when
``RunnerConfig.user`` is set, switches to that uid before exec (done in the
wrapper rather than via ``subprocess(user=...)``, which uvloop rejects).
"""
from __future__ import annotations
@@ -204,7 +205,7 @@ class ClaudeSdkBackend:
self._mcp_disallowed = _mcp_disallowed(agent, mcp_tool_names or {})
self._sessions: dict[str, _Live] = {}
self._reaper: asyncio.Task[None] | None = None
self._uid = _resolve_uid(self._runner.user)
self._uid, self._gid = _resolve_ids(self._runner.user)
self._wrapper: Path | None = None
@property
@@ -411,6 +412,7 @@ class ClaudeSdkBackend:
env = dict(opt.env)
if self._runner.home is not None:
env["HOME"] = str(self._runner.home)
env.setdefault("CLAUDE_CONFIG_DIR", str(self._runner.home / ".claude"))
plugins = self._plugins()
system_prompt = (
prompt_assembly.assemble(agent.prompt_sources)
@@ -430,7 +432,6 @@ class ClaudeSdkBackend:
cwd=str(agent.cwd),
add_dirs=list(opt.add_dirs),
env=env,
user=self._runner.user,
cli_path=str(self._exec_wrapper(extra_keep=tuple(env))),
include_partial_messages=opt.include_partial_messages,
session_store=self._store,
@@ -472,6 +473,8 @@ class ClaudeSdkBackend:
target=json.dumps(target),
keep=json.dumps(keep),
prefixes=json.dumps(list(ENV_KEEP_PREFIXES)),
uid=json.dumps(self._uid),
gid=json.dumps(self._gid),
)
digest = hashlib.sha256(script.encode("utf-8")).hexdigest()[:12]
path = self._work_dir / f"claude-exec-{digest}.py"
@@ -525,9 +528,15 @@ import sys
TARGET = {target}
KEEP = set({keep})
PREFIXES = tuple({prefixes})
UID = {uid}
GID = {gid}
env = {{
k: v for k, v in os.environ.items() if k in KEEP or k.startswith(PREFIXES)
}}
if UID is not None and os.getuid() != UID:
os.setgroups([])
os.setgid(GID)
os.setuid(UID)
os.execve(TARGET, [TARGET, *sys.argv[1:]], env)
"""
@@ -550,12 +559,11 @@ def _claude_binary() -> str:
return found
def _resolve_uid(user: str | None) -> int | None:
def _resolve_ids(user: str | None) -> tuple[int | None, int | None]:
if user is None:
return None
if user.isdigit():
return int(user)
return pwd.getpwnam(user).pw_uid
return None, None
record = pwd.getpwuid(int(user)) if user.isdigit() else pwd.getpwnam(user)
return record.pw_uid, record.pw_gid
def _chown_tree(root: Path, uid: int) -> None: