From add3f7a07a9b170e88dcf1cc0e813cafa7503b61 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone <8633216+AlexanderWhitestone@users.noreply.github.com> Date: Sat, 28 Feb 2026 07:39:31 -0500 Subject: [PATCH] fix: register task_request handler and fix Docker 403 errors on macOS (#86) --- Dockerfile | 5 ++++ docker-compose.yml | 6 +++++ src/dashboard/app.py | 42 +++++++++++++++++++++++++++++++++ tests/swarm/test_task_queue.py | 43 ++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/Dockerfile b/Dockerfile index 6ac6daa6..ca4bd0d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,11 @@ RUN mkdir -p /app/data # ── Non-root user for production ───────────────────────────────────────────── RUN groupadd -r timmy && useradd -r -g timmy -d /app -s /sbin/nologin timmy \ && chown -R timmy:timmy /app +# Ensure static/ and data/ are world-readable so bind-mounted files +# from the macOS host remain accessible when running as the timmy user. +# Docker Desktop for Mac bind mounts inherit host permissions, which may +# not include the container's timmy UID — chmod o+rX fixes 403 errors. +RUN chmod -R o+rX /app/static /app/data USER timmy # ── Environment ────────────────────────────────────────────────────────────── diff --git a/docker-compose.yml b/docker-compose.yml index c19bd55b..99046c54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,12 @@ services: build: . image: timmy-time:latest container_name: timmy-dashboard + # Run as root in the dev compose because bind-mounted host files + # (./src, ./static) may not be readable by the image's non-root + # "timmy" user — this is the #1 cause of 403 errors on macOS. + # Production (docker-compose.prod.yml) uses no bind mounts and + # correctly runs as the Dockerfile's non-root USER. + user: "0:0" ports: - "8000:8000" volumes: diff --git a/src/dashboard/app.py b/src/dashboard/app.py index c7d2c4b6..17adf4e7 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -231,11 +231,53 @@ async def _task_processor_loop() -> None: """Handler for bug_report tasks - acknowledge and mark completed.""" return f"Bug report acknowledged: {task.title}" + def handle_task_request(task): + """Handler for task_request tasks — user-queued work items from chat.""" + try: + now = datetime.now() + context = ( + f"[System: Current date/time is {now.strftime('%A, %B %d, %Y at %I:%M %p')}]\n" + f"[System: You have been assigned a task from the queue. " + f"Complete it and provide your response.]\n\n" + f"Task: {task.title}\n" + ) + if task.description and task.description != task.title: + context += f"Details: {task.description}\n" + + response = timmy_chat(context) + + # Push response to user via WebSocket + try: + from infrastructure.ws_manager.handler import ws_manager + + asyncio.create_task( + ws_manager.broadcast( + "timmy_response", + { + "task_id": task.id, + "response": response, + }, + ) + ) + except Exception as e: + logger.debug("Failed to push response via WS: %s", e) + + return response + except Exception as e: + logger.error("Task request failed: %s", e) + try: + from infrastructure.error_capture import capture_error + capture_error(e, source="task_request_handler") + except Exception: + pass + return f"Error: {str(e)}" + # Register handlers task_processor.register_handler("chat_response", handle_chat_response) task_processor.register_handler("thought", handle_thought) task_processor.register_handler("internal", handle_thought) task_processor.register_handler("bug_report", handle_bug_report) + task_processor.register_handler("task_request", handle_task_request) # ── Reconcile zombie tasks from previous crash ── zombie_count = task_processor.reconcile_zombie_tasks() diff --git a/tests/swarm/test_task_queue.py b/tests/swarm/test_task_queue.py index f72bd627..f217df28 100644 --- a/tests/swarm/test_task_queue.py +++ b/tests/swarm/test_task_queue.py @@ -876,6 +876,49 @@ class TestTaskProcessor: refreshed = get_task(task.id) assert refreshed.status == TaskStatus.APPROVED + @pytest.mark.asyncio + async def test_task_request_type_has_handler(self): + """task_request tasks are processed (not backlogged) when a handler is registered. + + Regression test: previously task_request had no handler, causing all + user-queued tasks from chat to be immediately backlogged. + """ + from swarm.task_processor import TaskProcessor + from swarm.task_queue.models import create_task, get_task, TaskStatus + + tp = TaskProcessor("task-request-test") + tp.register_handler("task_request", lambda task: f"Completed: {task.title}") + + task = create_task( + title="Refactor the login module", + description="Create a task to refactor the login module", + task_type="task_request", + assigned_to="task-request-test", + created_by="user", + ) + + result = await tp.process_single_task(task) + assert result is not None + + refreshed = get_task(task.id) + assert refreshed.status == TaskStatus.COMPLETED + assert "Refactor" in refreshed.result + + def test_chat_queue_request_creates_task_request_type(self, client): + """Chat messages that match queue patterns create task_request tasks.""" + from swarm.task_queue.models import list_tasks + + client.post( + "/agents/timmy/chat", + data={"message": "Add refactor the login module to the task queue"}, + ) + + tasks = list_tasks(assigned_to="timmy") + task_request_tasks = [t for t in tasks if t.task_type == "task_request"] + assert len(task_request_tasks) >= 1 + assert any("login" in t.title.lower() or "refactor" in t.title.lower() + for t in task_request_tasks) + # ── Backlog Route Tests ─────────────────────────────────────────────────