fix: ESC key, .config, workspace permissions, terminal race condition

This commit is contained in:
2026-06-01 23:16:06 +02:00
parent a0cfbbc2d2
commit 8837031fd2
4 changed files with 61 additions and 60 deletions
+20 -2
View File
@@ -118,6 +118,11 @@ def compile_dockerfile(manifest: dict) -> str:
# System packages (apt)
apt_packages = manifest.get("packages", {}).get("apt", [])
if manifest.get("user"):
# Ensure sudo is available for permission-fixing startup scripts
apt_packages = list(apt_packages)
if "sudo" not in apt_packages:
apt_packages.append("sudo")
if apt_packages:
lines.append("RUN apt-get update && apt-get install -y \\")
for pkg in apt_packages[:-1]:
@@ -168,10 +173,18 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append(f"ENV USER={name}")
lines.append("")
# Ensure home directory exists and is writable by the user
lines.append(f"RUN mkdir -p {home} && chown {name}:{name} {home} && chmod 755 {home}")
lines.append(
f"RUN mkdir -p {home} && chown {name}:{name} {home} && chmod 755 {home}"
)
lines.append("")
# Build scripts
# Configure passwordless sudo so startup scripts can fix permissions
lines.append(
f'RUN echo "{name} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/{name} && chmod 0440 /etc/sudoers.d/{name}'
)
lines.append("")
# Build scripts
build_scripts = manifest.get("scripts", {}).get("build", [])
for script in build_scripts:
# Normalize multi-line scripts into single RUN command
@@ -184,6 +197,11 @@ def compile_dockerfile(manifest: dict) -> str:
if build_scripts:
lines.append("")
# After build scripts, ensure everything in home is owned by the user
if user and build_scripts:
lines.append(f"RUN chown -R {name}:{name} {home}")
lines.append("")
# Create mount target directories
mounts = manifest.get("mounts", [])
if mounts:
+9 -4
View File
@@ -6,6 +6,7 @@ import uuid
from datetime import datetime, timezone
from fastapi import WebSocket
from sqlalchemy.dialects.postgresql import insert as pg_insert
from src.database import SessionLocal
from src.models.terminal_session import TerminalSessionModel
@@ -83,18 +84,22 @@ class TerminalManager:
instance_id: uuid.UUID,
name: str,
) -> None:
"""Insert a TerminalSessionModel row into the database."""
"""Insert a TerminalSessionModel row into the database.
Uses ON CONFLICT DO NOTHING to handle races when a session is
restored from DB and then re-inserted.
"""
try:
async with SessionLocal() as db_session:
db_row = TerminalSessionModel(
stmt = pg_insert(TerminalSessionModel).values(
id=uuid.UUID(session_id),
instance_id=instance_id,
name=name,
status="active",
created_at=datetime.now(timezone.utc),
last_activity_at=datetime.now(timezone.utc),
)
db_session.add(db_row)
).on_conflict_do_nothing(index_elements=["id"])
await db_session.execute(stmt)
await db_session.commit()
logger.debug(
"Inserted terminal session row %s for instance %s",
+30 -37
View File
@@ -2,9 +2,11 @@
from __future__ import annotations
import contextlib
import logging
import os
import shutil
import stat
import uuid
from dataclasses import dataclass
from datetime import datetime
@@ -76,10 +78,8 @@ class WorkspaceManager:
parent = os.path.dirname(path)
os.makedirs(parent, exist_ok=True)
# Ensure container users (various UIDs) can write to workspace dirs
try:
with contextlib.suppress(OSError):
os.chmod(parent, 0o777)
except OSError:
pass
logger.info(
"Creating workspace: name=%s, repo=%s, branch=%s", name, repo.id, branch
@@ -104,23 +104,7 @@ class WorkspaceManager:
).decode()
await GitService.clone(repo.remote_url, branch, path, ssh_key=ssh_key)
# Make workspace writable for any container user
try:
os.chmod(path, 0o777)
for root, dirs, files in os.walk(path):
for d in dirs:
try:
os.chmod(os.path.join(root, d), 0o777)
except OSError:
pass
for f in files:
try:
os.chmod(os.path.join(root, f), 0o666)
except OSError:
pass
except OSError:
logger.warning("Failed to chmod workspace path: %s", path)
self._make_world_writable(path)
workspace = Workspace(
name=name,
@@ -213,28 +197,37 @@ class WorkspaceManager:
return SyncResult(branch_deleted=True)
await GitService.pull(workspace.path, workspace.branch, ssh_key=ssh_key)
# Re-apply permissive permissions after sync
try:
os.chmod(workspace.path, 0o777)
for root, dirs, files in os.walk(workspace.path):
for d in dirs:
try:
os.chmod(os.path.join(root, d), 0o777)
except OSError:
pass
for f in files:
try:
os.chmod(os.path.join(root, f), 0o666)
except OSError:
pass
except OSError:
logger.warning("Failed to chmod workspace after sync: %s", workspace.path)
self._make_world_writable(workspace.path)
workspace.last_sync_at = datetime.now()
logger.info("Workspace synced: %s", workspace.id)
return SyncResult(branch_deleted=False)
def _make_world_writable(self, path: str) -> None:
"""Recursively make path readable/writable/traversable by any UID.
Directories get 777 (traversable). Files get rw for all while
preserving any existing execute bits.
"""
with contextlib.suppress(OSError):
os.chmod(path, 0o777)
for root, dirs, files in os.walk(path):
for d in dirs:
dpath = os.path.join(root, d)
with contextlib.suppress(OSError):
os.chmod(dpath, 0o777)
for f in files:
fpath = os.path.join(root, f)
with contextlib.suppress(OSError):
mode = os.stat(fpath).st_mode
# Preserve execute bits, ensure read+write for all
new_mode = (mode & stat.S_IXUSR) | 0o666
if mode & stat.S_IXGRP:
new_mode |= stat.S_IXGRP
if mode & stat.S_IXOTH:
new_mode |= stat.S_IXOTH
os.chmod(fpath, new_mode)
async def _get_instances(
self,
workspace: Workspace,