fix: persist workspace Python environments

This commit is contained in:
wuyang
2026-07-26 20:20:37 +08:00
parent bc30a480ad
commit 2fbb5698ac
14 changed files with 170 additions and 20 deletions
+31 -10
View File
@@ -122,13 +122,21 @@ class DockerExecutionProvider:
ref = workspace_ref(user_id)
def ensure():
try:
desired_image = self.client.images.get(self.settings.workspace_image)
except ImageNotFound as exc:
raise RuntimeError(f"Workspace image {self.settings.workspace_image!r} is not installed") from exc
try:
container = self.client.containers.get(ref.container_name)
container.reload()
configured_image = str(container.attrs.get("Config", {}).get("Image", ""))
running_image_id = str(container.attrs.get("Image", ""))
if configured_image != self.settings.workspace_image or running_image_id != desired_image.id:
container.remove(force=True)
container = None
except NotFound:
try:
self.client.images.get(self.settings.workspace_image)
except ImageNotFound as exc:
raise RuntimeError(f"Workspace image {self.settings.workspace_image!r} is not installed") from exc
container = None
if container is None:
if self.settings.workspace_network_enabled:
try:
self.client.networks.get(ref.network_name)
@@ -153,6 +161,7 @@ class DockerExecutionProvider:
labels={
"app.k1412.component": "user-workspace",
"app.k1412.workspace-id": ref.workspace_id,
"app.k1412.workspace-image": self.settings.workspace_image,
},
detach=True,
read_only=True,
@@ -222,8 +231,20 @@ class DockerExecutionProvider:
async def exec(self, user_id: str, command: str, cwd: str, timeout_seconds: int) -> ToolResult:
normalize_workspace_path(cwd)
bounded_timeout = max(1, min(timeout_seconds, 1800))
shell = f"timeout --signal=TERM {bounded_timeout}s sh -lc {shlex.quote(command)}"
return await self._exec_argv(user_id, ["sh", "-lc", shell], cwd=cwd)
return await self._exec_argv(
user_id,
[
"timeout",
"--signal=TERM",
f"{bounded_timeout}s",
"bash",
"-o",
"pipefail",
"-c",
command,
],
cwd=cwd,
)
async def list_files(self, user_id: str, path: str, max_depth: int, limit: int) -> ToolResult:
absolute = absolute_workspace_path(path)
@@ -233,7 +254,7 @@ class DockerExecutionProvider:
"base_depth=root.rstrip('/').count('/')\n"
"for current, dirs, files in os.walk(root):\n"
" depth=current.rstrip('/').count('/')-base_depth\n"
" dirs[:]=sorted(d for d in dirs if d not in {'.git','node_modules','.venv','__pycache__'})\n"
" dirs[:]=sorted(d for d in dirs if d not in {'.agent','.git','node_modules','.venv','__pycache__'})\n"
" if depth>=max_depth: dirs[:]=[]\n"
" rel=os.path.relpath(current,'/workspace')\n"
" for name in sorted(dirs): out.append((os.path.join(rel,name) if rel!='.' else name)+'/')\n"
@@ -408,19 +429,19 @@ class DockerExecutionProvider:
process_id = uuid.uuid4().hex
process_dir = absolute_workspace_path(f".agent/processes/{process_id}")
worker = (
f"sh -lc {shlex.quote(command)}; "
f"bash -o pipefail -c {shlex.quote(command)}; "
"code=$?; "
f'echo "$code" > {shlex.quote(process_dir + "/exit_code")}; '
'exit "$code"'
)
wrapped = (
f"mkdir -p {shlex.quote(process_dir)}; "
f"setsid sh -lc {shlex.quote(worker)} "
f"setsid bash -c {shlex.quote(worker)} "
f"> {shlex.quote(process_dir + '/output.log')} 2>&1 & "
f'pid=$!; echo "$pid" > {shlex.quote(process_dir + "/pid")}; '
f"echo {shlex.quote(json.dumps({'process_id': process_id}))}"
)
result = await self._exec_argv(user_id, ["sh", "-lc", wrapped], cwd=cwd)
result = await self._exec_argv(user_id, ["bash", "-o", "pipefail", "-c", wrapped], cwd=cwd)
result.metadata = {"process_id": process_id}
return result