From a0cfbbc2d2d9f697d199420071fce69925441351 Mon Sep 17 00:00:00 2001
From: Alex Blank
Date: Mon, 1 Jun 2026 22:36:31 +0200
Subject: [PATCH] fix: container mount permissions, terminal shift, ESC capture
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bug 1 — in-container repo mounting:
- docker-compose.yml: added /data/working-copies:/data/working-copies mount
to API container so workspace dirs are visible on host filesystem
- Dockerfile: create /data/working-copies dir in image
Bug 2 — /home/user not writable:
- workspace_manager.py: chmod 777 workspace dirs + 666 files after clone
and after sync, so any container user can write
- manifest_compiler.py: explicit mkdir + chown + chmod 755 for home dir
in generated Dockerfile
Bug 3 — terminal text shifts left on typing:
- terminal.tsx: removed manual term.refresh() after fit (caused reflow)
- Track lastSentCols/lastSentRows and only send resize when dimensions
actually changed, preventing resize feedback loops
Bug 4 — ESC key captured by terminal:
- terminal.tsx: attachCustomKeyEventHandler allows ESC to propagate to
browser when not in alternate buffer (vim/tmux), so modals/navigation
work; ESC still sent to PTY when in vim/tmux alternate screen
Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces (9 passed)
---
apps/api/Dockerfile | 4 +-
apps/api/src/services/manifest_compiler.py | 7 +++-
apps/api/src/services/workspace_manager.py | 43 +++++++++++++++++++++-
apps/web/src/components/start-tool-fab.tsx | 11 ++----
apps/web/src/components/terminal.tsx | 34 ++++++++++++-----
apps/web/src/pages/dashboard.tsx | 5 +--
apps/web/src/pages/sessions.tsx | 4 +-
apps/web/src/styles.css | 4 +-
docker-compose.yml | 1 +
9 files changed, 83 insertions(+), 30 deletions(-)
diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile
index b2d0cc5..fdbfe39 100644
--- a/apps/api/Dockerfile
+++ b/apps/api/Dockerfile
@@ -50,8 +50,8 @@ ENV PATH=/root/.local/bin:$PATH
# Copy application code
COPY --chown=appuser:appgroup . .
-# Create directories for repo and instance storage
-RUN mkdir -p /data/repos /data/instances && chown -R appuser:appgroup /data
+# Create directories for repo, instance, and workspace storage
+RUN mkdir -p /data/repos /data/instances /data/working-copies && chown -R appuser:appgroup /data
# Copy wait-for-db script
COPY wait-for-db.sh /usr/local/bin/wait-for-db.sh
diff --git a/apps/api/src/services/manifest_compiler.py b/apps/api/src/services/manifest_compiler.py
index 11f18a8..a971268 100644
--- a/apps/api/src/services/manifest_compiler.py
+++ b/apps/api/src/services/manifest_compiler.py
@@ -26,7 +26,7 @@ def resolve_base(manifest: dict) -> dict:
result = deepcopy(manifest)
base_definition_id = result.pop("base_definition_id", None)
- base_version = result.pop("base_version", "latest")
+ result.pop("base_version", None)
if base_definition_id:
# This will be provided by the caller (they have the DB session)
@@ -167,8 +167,11 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append(f"ENV HOME={home}")
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("")
- # Build scripts
+ # Build scripts
build_scripts = manifest.get("scripts", {}).get("build", [])
for script in build_scripts:
# Normalize multi-line scripts into single RUN command
diff --git a/apps/api/src/services/workspace_manager.py b/apps/api/src/services/workspace_manager.py
index a37c703..ba1fb37 100644
--- a/apps/api/src/services/workspace_manager.py
+++ b/apps/api/src/services/workspace_manager.py
@@ -73,7 +73,13 @@ class WorkspaceManager:
RuntimeError: If git clone fails.
"""
path = self._workspace_path(repo.id, name)
- os.makedirs(os.path.dirname(path), exist_ok=True)
+ parent = os.path.dirname(path)
+ os.makedirs(parent, exist_ok=True)
+ # Ensure container users (various UIDs) can write to workspace dirs
+ try:
+ os.chmod(parent, 0o777)
+ except OSError:
+ pass
logger.info(
"Creating workspace: name=%s, repo=%s, branch=%s", name, repo.id, branch
@@ -99,6 +105,23 @@ class WorkspaceManager:
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)
+
workspace = Workspace(
name=name,
repo_id=repo.id,
@@ -190,6 +213,24 @@ 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)
+
workspace.last_sync_at = datetime.now()
logger.info("Workspace synced: %s", workspace.id)
return SyncResult(branch_deleted=False)
diff --git a/apps/web/src/components/start-tool-fab.tsx b/apps/web/src/components/start-tool-fab.tsx
index 4741fb7..36e0bcc 100644
--- a/apps/web/src/components/start-tool-fab.tsx
+++ b/apps/web/src/components/start-tool-fab.tsx
@@ -71,16 +71,12 @@ export function StartToolFAB() {
) : !selectedWorkspace ? (
-
+